From 9fc0e1566e34c6dacb774326438bae4eeee1e7bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Calisto?= Date: Fri, 13 Oct 2023 11:54:34 +0100 Subject: [PATCH] Feature Management: Update admin page UI after a successful update (#76380) * Feature Management: Update admin page UI after a successful update * lint * lint * refactor --- pkg/api/api.go | 1 + pkg/api/featuremgmt.go | 7 +++ pkg/services/featuremgmt/manager.go | 23 +++++--- pkg/services/featuremgmt/models.go | 4 ++ .../features/admin/AdminFeatureTogglesAPI.ts | 11 +++- .../admin/AdminFeatureTogglesPage.tsx | 52 +++++++++++++++++-- .../admin/AdminFeatureTogglesTable.tsx | 22 +++++--- 7 files changed, 101 insertions(+), 19 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 9ff29328060..437499a6f54 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -412,6 +412,7 @@ func (hs *HTTPServer) registerRoutes() { if hs.Features.IsEnabled(featuremgmt.FlagFeatureToggleAdminPage) { apiRoute.Group("/featuremgmt", func(featuremgmtRoute routing.RouteRegister) { + featuremgmtRoute.Get("/state", authorize(ac.EvalPermission(ac.ActionFeatureManagementRead)), hs.GetFeatureMgmtState) featuremgmtRoute.Get("/", authorize(ac.EvalPermission(ac.ActionFeatureManagementRead)), hs.GetFeatureToggles) featuremgmtRoute.Post("/", authorize(ac.EvalPermission(ac.ActionFeatureManagementWrite)), hs.UpdateFeatureToggle) }) diff --git a/pkg/api/featuremgmt.go b/pkg/api/featuremgmt.go index a3fd7182666..d9586a98d67 100644 --- a/pkg/api/featuremgmt.go +++ b/pkg/api/featuremgmt.go @@ -78,9 +78,16 @@ func (hs *HTTPServer) UpdateFeatureToggle(ctx *contextmodel.ReqContext) response return response.Respond(http.StatusBadRequest, "Failed to perform webhook request") } + hs.Features.SetRestartRequired() + return response.Respond(http.StatusOK, "feature toggles updated successfully") } +func (hs *HTTPServer) GetFeatureMgmtState(ctx *contextmodel.ReqContext) response.Response { + fmState := hs.Features.GetState() + return response.Respond(http.StatusOK, fmState) +} + // isFeatureHidden returns whether a toggle should be hidden from the admin page. // filters out statuses Unknown, Experimental, and Private Preview func isFeatureHidden(flag featuremgmt.FeatureFlag, hideCfg map[string]struct{}) bool { diff --git a/pkg/services/featuremgmt/manager.go b/pkg/services/featuremgmt/manager.go index 6e82d2936cc..c4e47adcc3d 100644 --- a/pkg/services/featuremgmt/manager.go +++ b/pkg/services/featuremgmt/manager.go @@ -14,13 +14,14 @@ var ( ) type FeatureManager struct { - isDevMod bool - licensing licensing.Licensing - flags map[string]*FeatureFlag - enabled map[string]bool // only the "on" values - config string // path to config file - vars map[string]any - log log.Logger + isDevMod bool + restartRequired bool + licensing licensing.Licensing + flags map[string]*FeatureFlag + enabled map[string]bool // only the "on" values + config string // path to config file + vars map[string]any + log log.Logger } // This will merge the flags with the current configuration @@ -148,6 +149,14 @@ func (fm *FeatureManager) GetFlags() []FeatureFlag { return v } +func (fm *FeatureManager) GetState() *FeatureManagerState { + return &FeatureManagerState{RestartRequired: fm.restartRequired} +} + +func (fm *FeatureManager) SetRestartRequired() { + fm.restartRequired = true +} + // Check to see if a feature toggle exists by name func (fm *FeatureManager) LookupFlag(name string) (FeatureFlag, bool) { f, ok := fm.flags[name] diff --git a/pkg/services/featuremgmt/models.go b/pkg/services/featuremgmt/models.go index 59c1e66cdf2..0666ed9a2ce 100644 --- a/pkg/services/featuremgmt/models.go +++ b/pkg/services/featuremgmt/models.go @@ -127,3 +127,7 @@ type FeatureToggleDTO struct { Enabled bool `json:"enabled"` ReadOnly bool `json:"readOnly,omitempty"` } + +type FeatureManagerState struct { + RestartRequired bool `json:"restartRequired"` +} diff --git a/public/app/features/admin/AdminFeatureTogglesAPI.ts b/public/app/features/admin/AdminFeatureTogglesAPI.ts index 35a67cd455a..7c13e775172 100644 --- a/public/app/features/admin/AdminFeatureTogglesAPI.ts +++ b/public/app/features/admin/AdminFeatureTogglesAPI.ts @@ -30,6 +30,9 @@ export const togglesApi = createApi({ reducerPath: 'togglesApi', baseQuery: backendSrvBaseQuery({ baseUrl: '/api' }), endpoints: (builder) => ({ + getManagerState: builder.query({ + query: () => ({ url: '/featuremgmt/state' }), + }), getFeatureToggles: builder.query({ query: () => ({ url: '/featuremgmt' }), }), @@ -50,5 +53,9 @@ type FeatureToggle = { readOnly?: boolean; }; -export const { useGetFeatureTogglesQuery, useUpdateFeatureTogglesMutation } = togglesApi; -export type { FeatureToggle }; +type FeatureMgmtState = { + restartRequired: boolean; +}; + +export const { useGetManagerStateQuery, useGetFeatureTogglesQuery, useUpdateFeatureTogglesMutation } = togglesApi; +export type { FeatureToggle, FeatureMgmtState }; diff --git a/public/app/features/admin/AdminFeatureTogglesPage.tsx b/public/app/features/admin/AdminFeatureTogglesPage.tsx index 36bdfca4f2c..138b6a28217 100644 --- a/public/app/features/admin/AdminFeatureTogglesPage.tsx +++ b/public/app/features/admin/AdminFeatureTogglesPage.tsx @@ -1,26 +1,72 @@ -import React from 'react'; +import { css } from '@emotion/css'; +import React, { useState } from 'react'; +import { GrafanaTheme2 } from '@grafana/data'; +import { useStyles2, Icon } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; -import { useGetFeatureTogglesQuery } from './AdminFeatureTogglesAPI'; +import { useGetFeatureTogglesQuery, useGetManagerStateQuery } from './AdminFeatureTogglesAPI'; import { AdminFeatureTogglesTable } from './AdminFeatureTogglesTable'; export default function AdminFeatureTogglesPage() { const { data: featureToggles, isLoading, isError } = useGetFeatureTogglesQuery(); + const { data: featureMgmtState } = useGetManagerStateQuery(); + const [updateSuccessful, setUpdateSuccessful] = useState(false); + + const styles = useStyles2(getStyles); const getErrorMessage = () => { return 'Error fetching feature toggles'; }; + const handleUpdateSuccess = () => { + setUpdateSuccessful(true); + }; + + const AlertMessage = () => { + return ( +
+
+ +
+ + {featureMgmtState?.restartRequired || updateSuccessful + ? 'A restart is pending for your Grafana instance to apply the latest feature toggle changes' + : 'Saving feature toggle changes will prompt a restart of the instance, which may take a few minutes'} + +
+ ); + }; + return ( <> {isError && getErrorMessage()} {isLoading && 'Fetching feature toggles'} - {featureToggles && } + + {featureToggles && ( + + )} ); } + +function getStyles(theme: GrafanaTheme2) { + return { + warning: css({ + display: 'flex', + marginTop: theme.spacing(3), + }), + 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 index eca6021bf31..bcfbf831fc2 100644 --- a/public/app/features/admin/AdminFeatureTogglesTable.tsx +++ b/public/app/features/admin/AdminFeatureTogglesTable.tsx @@ -6,6 +6,7 @@ import { type FeatureToggle, useUpdateFeatureTogglesMutation } from './AdminFeat interface Props { featureToggles: FeatureToggle[]; + onUpdateSuccess: () => void; } const sortByName: SortByFn = (a, b) => { @@ -27,10 +28,11 @@ const sortByEnabled: SortByFn = (a, b) => { return a.original.enabled === b.original.enabled ? 0 : a.original.enabled ? 1 : -1; }; -export function AdminFeatureTogglesTable({ featureToggles }: Props) { +export function AdminFeatureTogglesTable({ featureToggles, onUpdateSuccess }: Props) { const [localToggles, setLocalToggles] = useState(featureToggles); const [updateFeatureToggles] = useUpdateFeatureTogglesMutation(); const [modifiedToggles, setModifiedToggles] = useState([]); + const [isSaving, setIsSaving] = useState(false); const handleToggleChange = (toggle: FeatureToggle, newValue: boolean) => { const updatedToggle = { ...toggle, enabled: newValue }; @@ -56,10 +58,16 @@ export function AdminFeatureTogglesTable({ featureToggles }: Props) { }; const handleSaveChanges = async () => { - const resp = await updateFeatureToggles(modifiedToggles); - // Reset modifiedToggles after successful update - if (!('error' in resp)) { - setModifiedToggles([]); + setIsSaving(true); + try { + const resp = await updateFeatureToggles(modifiedToggles); + // Reset modifiedToggles after successful update + if (!('error' in resp)) { + onUpdateSuccess(); + setModifiedToggles([]); + } + } finally { + setIsSaving(false); } }; @@ -103,8 +111,8 @@ export function AdminFeatureTogglesTable({ featureToggles }: Props) { return ( <>
-
featureToggle.name} />