From 7869ca1932c3a2a8f233acf35a3fe676187847bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Calisto?= Date: Thu, 26 Oct 2023 10:42:00 +0100 Subject: [PATCH] Feature Management: UI improvements (#76866) * Feature Management: UI improvements * update UI logic --------- Co-authored-by: Michael Mandrus --- pkg/api/featuremgmt.go | 4 + pkg/services/featuremgmt/manager.go | 3 +- pkg/services/featuremgmt/models.go | 1 + pkg/services/featuremgmt/service.go | 11 +-- .../features/admin/AdminFeatureTogglesAPI.ts | 1 + .../admin/AdminFeatureTogglesPage.tsx | 13 +++- .../admin/AdminFeatureTogglesTable.tsx | 76 ++++++++++--------- 7 files changed, 62 insertions(+), 47 deletions(-) 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} /> );