diff --git a/pkg/api/featuremgmt.go b/pkg/api/featuremgmt.go
index d9586a98d67..da34568366f 100644
--- a/pkg/api/featuremgmt.go
+++ b/pkg/api/featuremgmt.go
@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
+ "sort"
"strconv"
"github.com/grafana/grafana/pkg/api/response"
@@ -36,6 +37,9 @@ func (hs *HTTPServer) GetFeatureToggles(ctx *contextmodel.ReqContext) response.R
}
dtos = append(dtos, dto)
+ sort.Slice(dtos, func(i, j int) bool {
+ return dtos[i].Name < dtos[j].Name
+ })
}
return response.JSON(http.StatusOK, dtos)
diff --git a/pkg/services/featuremgmt/manager.go b/pkg/services/featuremgmt/manager.go
index c4e47adcc3d..95ab6180580 100644
--- a/pkg/services/featuremgmt/manager.go
+++ b/pkg/services/featuremgmt/manager.go
@@ -16,6 +16,7 @@ var (
type FeatureManager struct {
isDevMod bool
restartRequired bool
+ allowEditing bool
licensing licensing.Licensing
flags map[string]*FeatureFlag
enabled map[string]bool // only the "on" values
@@ -150,7 +151,7 @@ func (fm *FeatureManager) GetFlags() []FeatureFlag {
}
func (fm *FeatureManager) GetState() *FeatureManagerState {
- return &FeatureManagerState{RestartRequired: fm.restartRequired}
+ return &FeatureManagerState{RestartRequired: fm.restartRequired, AllowEditing: fm.allowEditing}
}
func (fm *FeatureManager) SetRestartRequired() {
diff --git a/pkg/services/featuremgmt/models.go b/pkg/services/featuremgmt/models.go
index 7f0324f6844..f2aaa7b1c19 100644
--- a/pkg/services/featuremgmt/models.go
+++ b/pkg/services/featuremgmt/models.go
@@ -132,4 +132,5 @@ type FeatureToggleDTO struct {
type FeatureManagerState struct {
RestartRequired bool `json:"restartRequired"`
+ AllowEditing bool `json:"allowEditing"`
}
diff --git a/pkg/services/featuremgmt/service.go b/pkg/services/featuremgmt/service.go
index 7a8b2db0fbd..b8b7f0ce6d6 100644
--- a/pkg/services/featuremgmt/service.go
+++ b/pkg/services/featuremgmt/service.go
@@ -24,11 +24,12 @@ var (
func ProvideManagerService(cfg *setting.Cfg, licensing licensing.Licensing) (*FeatureManager, error) {
mgmt := &FeatureManager{
- isDevMod: setting.Env != setting.Prod,
- licensing: licensing,
- flags: make(map[string]*FeatureFlag, 30),
- enabled: make(map[string]bool),
- log: log.New("featuremgmt"),
+ isDevMod: setting.Env != setting.Prod,
+ licensing: licensing,
+ flags: make(map[string]*FeatureFlag, 30),
+ enabled: make(map[string]bool),
+ allowEditing: cfg.FeatureManagement.AllowEditing && cfg.FeatureManagement.UpdateWebhook != "",
+ log: log.New("featuremgmt"),
}
// Register the standard flags
diff --git a/public/app/features/admin/AdminFeatureTogglesAPI.ts b/public/app/features/admin/AdminFeatureTogglesAPI.ts
index 7c13e775172..889f6f0759e 100644
--- a/public/app/features/admin/AdminFeatureTogglesAPI.ts
+++ b/public/app/features/admin/AdminFeatureTogglesAPI.ts
@@ -55,6 +55,7 @@ type FeatureToggle = {
type FeatureMgmtState = {
restartRequired: boolean;
+ allowEditing: boolean;
};
export const { useGetManagerStateQuery, useGetFeatureTogglesQuery, useUpdateFeatureTogglesMutation } = togglesApi;
diff --git a/public/app/features/admin/AdminFeatureTogglesPage.tsx b/public/app/features/admin/AdminFeatureTogglesPage.tsx
index 138b6a28217..4d962d8b67e 100644
--- a/public/app/features/admin/AdminFeatureTogglesPage.tsx
+++ b/public/app/features/admin/AdminFeatureTogglesPage.tsx
@@ -23,7 +23,7 @@ export default function AdminFeatureTogglesPage() {
setUpdateSuccessful(true);
};
- const AlertMessage = () => {
+ const EditingAlert = () => {
return (
@@ -44,9 +44,13 @@ export default function AdminFeatureTogglesPage() {
<>
{isError && getErrorMessage()}
{isLoading && 'Fetching feature toggles'}
-
+ {featureMgmtState?.allowEditing &&
}
{featureToggles && (
-
+
)}
>
@@ -58,7 +62,8 @@ function getStyles(theme: GrafanaTheme2) {
return {
warning: css({
display: 'flex',
- marginTop: theme.spacing(3),
+ marginTop: theme.spacing(0.25),
+ marginBottom: theme.spacing(0.25),
}),
icon: css({
color: theme.colors.warning.main,
diff --git a/public/app/features/admin/AdminFeatureTogglesTable.tsx b/public/app/features/admin/AdminFeatureTogglesTable.tsx
index bcfbf831fc2..d67a60c71eb 100644
--- a/public/app/features/admin/AdminFeatureTogglesTable.tsx
+++ b/public/app/features/admin/AdminFeatureTogglesTable.tsx
@@ -1,11 +1,12 @@
-import React, { useState } from 'react';
+import React, { useState, useRef } from 'react';
-import { Switch, InteractiveTable, type CellProps, Button, type SortByFn } from '@grafana/ui';
+import { Switch, InteractiveTable, Tooltip, type CellProps, Button, type SortByFn } from '@grafana/ui';
import { type FeatureToggle, useUpdateFeatureTogglesMutation } from './AdminFeatureTogglesAPI';
interface Props {
featureToggles: FeatureToggle[];
+ allowEditing: boolean;
onUpdateSuccess: () => void;
}
@@ -28,10 +29,10 @@ const sortByEnabled: SortByFn
= (a, b) => {
return a.original.enabled === b.original.enabled ? 0 : a.original.enabled ? 1 : -1;
};
-export function AdminFeatureTogglesTable({ featureToggles, onUpdateSuccess }: Props) {
+export function AdminFeatureTogglesTable({ featureToggles, allowEditing, onUpdateSuccess }: Props) {
+ const serverToggles = useRef(featureToggles);
const [localToggles, setLocalToggles] = useState(featureToggles);
const [updateFeatureToggles] = useUpdateFeatureTogglesMutation();
- const [modifiedToggles, setModifiedToggles] = useState([]);
const [isSaving, setIsSaving] = useState(false);
const handleToggleChange = (toggle: FeatureToggle, newValue: boolean) => {
@@ -40,43 +41,40 @@ export function AdminFeatureTogglesTable({ featureToggles, onUpdateSuccess }: Pr
// Update the local state
const updatedToggles = localToggles.map((t) => (t.name === toggle.name ? updatedToggle : t));
setLocalToggles(updatedToggles);
-
- // Check if the toggle exists in modifiedToggles
- const existingToggle = modifiedToggles.find((t) => t.name === toggle.name);
-
- // If it exists and its state is the same as the updated one, remove it from modifiedToggles
- if (existingToggle && existingToggle.enabled === newValue) {
- setModifiedToggles((prev) => prev.filter((t) => t.name !== toggle.name));
- } else {
- // Else, add/update the toggle in modifiedToggles
- setModifiedToggles((prev) => {
- const newToggles = prev.filter((t) => t.name !== toggle.name);
- newToggles.push(updatedToggle);
- return newToggles;
- });
- }
};
const handleSaveChanges = async () => {
setIsSaving(true);
try {
+ const modifiedToggles = getModifiedToggles();
const resp = await updateFeatureToggles(modifiedToggles);
- // Reset modifiedToggles after successful update
if (!('error' in resp)) {
+ // server toggles successfully updated
+ serverToggles.current = [...localToggles];
onUpdateSuccess();
- setModifiedToggles([]);
}
} finally {
setIsSaving(false);
}
};
+ 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 featureToggles.some((originalToggle) => {
- const modifiedToggle = localToggles.find((t) => t.name === originalToggle.name);
- return modifiedToggle && modifiedToggle.enabled !== originalToggle.enabled;
- });
+ 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 'Preview features are not editable';
+ }
+ return '';
};
const columns = [
@@ -96,13 +94,15 @@ export function AdminFeatureTogglesTable({ featureToggles, onUpdateSuccess }: Pr
id: 'enabled',
header: 'State',
cell: ({ row }: CellProps) => (
-
- handleToggleChange(row.original, e.currentTarget.checked)}
- />
-
+
+
+ handleToggleChange(row.original, e.currentTarget.checked)}
+ />
+
+
),
sortType: sortByEnabled,
},
@@ -110,11 +110,13 @@ export function AdminFeatureTogglesTable({ featureToggles, onUpdateSuccess }: Pr
return (
<>
-
-
-
+ {allowEditing && (
+
+
+
+ )}
featureToggle.name} />
>
);