FeatureToggles: Removed deprecated code (#111750)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
@@ -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}
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
+7
-1
@@ -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
|
||||
@@ -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{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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("")
|
||||
}
|
||||
@@ -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<BackendSrvRequest>
|
||||
) {
|
||||
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<BackendSrvRequest>) {
|
||||
this.apiCalls.push({
|
||||
url: url,
|
||||
method: 'post',
|
||||
});
|
||||
return await {};
|
||||
}
|
||||
|
||||
async patch(url: string, data: unknown, options?: Partial<BackendSrvRequest>) {
|
||||
this.apiCalls.push({
|
||||
url: url,
|
||||
method: 'patch',
|
||||
});
|
||||
return await {};
|
||||
}
|
||||
|
||||
// these aren't needed for this test
|
||||
async put(url: string, data: unknown, options?: Partial<BackendSrvRequest>) {
|
||||
return await {};
|
||||
}
|
||||
async delete(url: string, data?: unknown, options?: Partial<BackendSrvRequest>) {
|
||||
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));
|
||||
});
|
||||
});
|
||||
@@ -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<CurrentTogglesState>;
|
||||
updateFeatureToggles(toggles: FeatureToggle[]): Promise<void>;
|
||||
}
|
||||
|
||||
class K8sAPI implements FeatureTogglesAPI {
|
||||
baseURL = '/apis/featuretoggle.grafana.app/v0alpha1';
|
||||
|
||||
async getFeatureToggles(): Promise<CurrentTogglesState> {
|
||||
const current = await getBackendSrv().get<ResolvedToggleState>(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<void> {
|
||||
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();
|
||||
};
|
||||
@@ -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 (
|
||||
<div className={styles.warning}>
|
||||
<div className={styles.icon}>
|
||||
<Icon name="exclamation-triangle" />
|
||||
</div>
|
||||
<span className={styles.message}>
|
||||
{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'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const subTitle = (
|
||||
<div>
|
||||
<Trans i18nKey="admin.feature-toggles.sub-title">
|
||||
View and edit feature toggles. Read more about feature toggles at{' '}
|
||||
<TextLink
|
||||
href="https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/"
|
||||
external
|
||||
>
|
||||
grafana.com
|
||||
</TextLink>
|
||||
.
|
||||
</Trans>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Page navId="feature-toggles" subTitle={subTitle}>
|
||||
<Page.Contents isLoading={featureState.loading}>
|
||||
<>
|
||||
{featureState.error?.message}
|
||||
{featureState.loading && 'Fetching feature toggles'}
|
||||
|
||||
<EditingAlert />
|
||||
{featureState.value && (
|
||||
<AdminFeatureTogglesTable
|
||||
featureToggles={featureState.value.toggles}
|
||||
allowEditing={featureState.value.allowEditing || false}
|
||||
onUpdateSuccess={handleUpdateSuccess}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -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<FeatureToggle> = (a, b) => {
|
||||
return a.original.name.localeCompare(b.original.name);
|
||||
};
|
||||
|
||||
const sortByDescription: SortByFn<FeatureToggle> = (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<FeatureToggle> = (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<FeatureToggle[]>(featureToggles);
|
||||
const [localToggles, setLocalToggles] = useState<FeatureToggle[]>(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<HTMLButtonElement | null>(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 (
|
||||
<Tooltip
|
||||
content={t(
|
||||
'admin.admin-feature-toggles-table.get-stage-cell.content-general-availability',
|
||||
'General availability'
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<Trans i18nKey="admin.admin-feature-toggles-table.get-stage-cell.ga">GA</Trans>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
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<FeatureToggle, string>) => <div>{value}</div>,
|
||||
sortType: sortByName,
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
header: 'Description',
|
||||
cell: ({ cell: { value } }: CellProps<FeatureToggle, string>) => <div>{value}</div>,
|
||||
sortType: sortByDescription,
|
||||
},
|
||||
{
|
||||
id: 'stage',
|
||||
header: 'Stage',
|
||||
cell: ({ cell: { value } }: CellProps<FeatureToggle, string>) => <div>{getStageCell(value)}</div>,
|
||||
},
|
||||
{
|
||||
id: 'enabled',
|
||||
header: 'State',
|
||||
cell: ({ row }: CellProps<FeatureToggle, boolean>) => {
|
||||
const renderStateSwitch = (
|
||||
<div>
|
||||
<Switch
|
||||
value={row.original.enabled}
|
||||
disabled={row.original.readOnly}
|
||||
onChange={(e) => handleToggleChange(row.original, e.currentTarget.checked)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return row.original.readOnly ? (
|
||||
<Tooltip content={getToggleTooltipContent(row.original.readOnly)}>{renderStateSwitch}</Tooltip>
|
||||
) : (
|
||||
renderStateSwitch
|
||||
);
|
||||
},
|
||||
sortType: sortByEnabled,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{allowEditing && (
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '0 0 5px 0' }}>
|
||||
<Button disabled={!hasModifications() || isSaving} onClick={showSaveChangesModal(true)} ref={saveButtonRef}>
|
||||
{isSaving
|
||||
? t('admin.admin-feature-toggles-table.saving', 'Saving...')
|
||||
: t('admin.admin-feature-toggles-table.save-changes', 'Save changes')}
|
||||
</Button>
|
||||
<ConfirmModal
|
||||
isOpen={showSaveModel}
|
||||
title={t(
|
||||
'admin.admin-feature-toggles-table.title-apply-feature-toggle-changes',
|
||||
'Apply feature toggle changes'
|
||||
)}
|
||||
body={
|
||||
<div>
|
||||
<p>
|
||||
<Trans i18nKey="admin.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.
|
||||
</Trans>
|
||||
</p>
|
||||
<p>
|
||||
<Trans i18nKey="admin.admin-feature-toggles-table.confirm-modal-body-2">
|
||||
We advise understanding the implications of each feature change before making modifications.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
confirmText={t('admin.admin-feature-toggles-table.confirmText-save-changes', 'Save changes')}
|
||||
onConfirm={async () => {
|
||||
showSaveChangesModal(false)();
|
||||
handleSaveChanges();
|
||||
}}
|
||||
onDismiss={showSaveChangesModal(false)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<InteractiveTable columns={columns} data={localToggles} getRowId={(featureToggle) => featureToggle.name} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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')
|
||||
)
|
||||
: () => <Navigate replace to="/admin" />,
|
||||
},
|
||||
{
|
||||
path: '/admin/stats',
|
||||
component: SafeDynamicImport(
|
||||
|
||||
@@ -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</2>."
|
||||
},
|
||||
"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.",
|
||||
|
||||
Reference in New Issue
Block a user