mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 04:42:07 +08:00
Feature Management: Update admin page UI after a successful update (#76380)
* Feature Management: Update admin page UI after a successful update * lint * lint * refactor
This commit is contained in:
@ -412,6 +412,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
|
|
||||||
if hs.Features.IsEnabled(featuremgmt.FlagFeatureToggleAdminPage) {
|
if hs.Features.IsEnabled(featuremgmt.FlagFeatureToggleAdminPage) {
|
||||||
apiRoute.Group("/featuremgmt", func(featuremgmtRoute routing.RouteRegister) {
|
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.Get("/", authorize(ac.EvalPermission(ac.ActionFeatureManagementRead)), hs.GetFeatureToggles)
|
||||||
featuremgmtRoute.Post("/", authorize(ac.EvalPermission(ac.ActionFeatureManagementWrite)), hs.UpdateFeatureToggle)
|
featuremgmtRoute.Post("/", authorize(ac.EvalPermission(ac.ActionFeatureManagementWrite)), hs.UpdateFeatureToggle)
|
||||||
})
|
})
|
||||||
|
@ -78,9 +78,16 @@ func (hs *HTTPServer) UpdateFeatureToggle(ctx *contextmodel.ReqContext) response
|
|||||||
return response.Respond(http.StatusBadRequest, "Failed to perform webhook request")
|
return response.Respond(http.StatusBadRequest, "Failed to perform webhook request")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hs.Features.SetRestartRequired()
|
||||||
|
|
||||||
return response.Respond(http.StatusOK, "feature toggles updated successfully")
|
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.
|
// isFeatureHidden returns whether a toggle should be hidden from the admin page.
|
||||||
// filters out statuses Unknown, Experimental, and Private Preview
|
// filters out statuses Unknown, Experimental, and Private Preview
|
||||||
func isFeatureHidden(flag featuremgmt.FeatureFlag, hideCfg map[string]struct{}) bool {
|
func isFeatureHidden(flag featuremgmt.FeatureFlag, hideCfg map[string]struct{}) bool {
|
||||||
|
@ -14,13 +14,14 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type FeatureManager struct {
|
type FeatureManager struct {
|
||||||
isDevMod bool
|
isDevMod bool
|
||||||
licensing licensing.Licensing
|
restartRequired bool
|
||||||
flags map[string]*FeatureFlag
|
licensing licensing.Licensing
|
||||||
enabled map[string]bool // only the "on" values
|
flags map[string]*FeatureFlag
|
||||||
config string // path to config file
|
enabled map[string]bool // only the "on" values
|
||||||
vars map[string]any
|
config string // path to config file
|
||||||
log log.Logger
|
vars map[string]any
|
||||||
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// This will merge the flags with the current configuration
|
// This will merge the flags with the current configuration
|
||||||
@ -148,6 +149,14 @@ func (fm *FeatureManager) GetFlags() []FeatureFlag {
|
|||||||
return v
|
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
|
// Check to see if a feature toggle exists by name
|
||||||
func (fm *FeatureManager) LookupFlag(name string) (FeatureFlag, bool) {
|
func (fm *FeatureManager) LookupFlag(name string) (FeatureFlag, bool) {
|
||||||
f, ok := fm.flags[name]
|
f, ok := fm.flags[name]
|
||||||
|
@ -127,3 +127,7 @@ type FeatureToggleDTO struct {
|
|||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
ReadOnly bool `json:"readOnly,omitempty"`
|
ReadOnly bool `json:"readOnly,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FeatureManagerState struct {
|
||||||
|
RestartRequired bool `json:"restartRequired"`
|
||||||
|
}
|
||||||
|
@ -30,6 +30,9 @@ export const togglesApi = createApi({
|
|||||||
reducerPath: 'togglesApi',
|
reducerPath: 'togglesApi',
|
||||||
baseQuery: backendSrvBaseQuery({ baseUrl: '/api' }),
|
baseQuery: backendSrvBaseQuery({ baseUrl: '/api' }),
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
|
getManagerState: builder.query<FeatureMgmtState, void>({
|
||||||
|
query: () => ({ url: '/featuremgmt/state' }),
|
||||||
|
}),
|
||||||
getFeatureToggles: builder.query<FeatureToggle[], void>({
|
getFeatureToggles: builder.query<FeatureToggle[], void>({
|
||||||
query: () => ({ url: '/featuremgmt' }),
|
query: () => ({ url: '/featuremgmt' }),
|
||||||
}),
|
}),
|
||||||
@ -50,5 +53,9 @@ type FeatureToggle = {
|
|||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const { useGetFeatureTogglesQuery, useUpdateFeatureTogglesMutation } = togglesApi;
|
type FeatureMgmtState = {
|
||||||
export type { FeatureToggle };
|
restartRequired: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const { useGetManagerStateQuery, useGetFeatureTogglesQuery, useUpdateFeatureTogglesMutation } = togglesApi;
|
||||||
|
export type { FeatureToggle, FeatureMgmtState };
|
||||||
|
@ -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 { Page } from 'app/core/components/Page/Page';
|
||||||
|
|
||||||
import { useGetFeatureTogglesQuery } from './AdminFeatureTogglesAPI';
|
import { useGetFeatureTogglesQuery, useGetManagerStateQuery } from './AdminFeatureTogglesAPI';
|
||||||
import { AdminFeatureTogglesTable } from './AdminFeatureTogglesTable';
|
import { AdminFeatureTogglesTable } from './AdminFeatureTogglesTable';
|
||||||
|
|
||||||
export default function AdminFeatureTogglesPage() {
|
export default function AdminFeatureTogglesPage() {
|
||||||
const { data: featureToggles, isLoading, isError } = useGetFeatureTogglesQuery();
|
const { data: featureToggles, isLoading, isError } = useGetFeatureTogglesQuery();
|
||||||
|
const { data: featureMgmtState } = useGetManagerStateQuery();
|
||||||
|
const [updateSuccessful, setUpdateSuccessful] = useState(false);
|
||||||
|
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const getErrorMessage = () => {
|
const getErrorMessage = () => {
|
||||||
return 'Error fetching feature toggles';
|
return 'Error fetching feature toggles';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdateSuccess = () => {
|
||||||
|
setUpdateSuccessful(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AlertMessage = () => {
|
||||||
|
return (
|
||||||
|
<div className={styles.warning}>
|
||||||
|
<div className={styles.icon}>
|
||||||
|
<Icon name="exclamation-triangle" />
|
||||||
|
</div>
|
||||||
|
<span className={styles.message}>
|
||||||
|
{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'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navId="feature-toggles">
|
<Page navId="feature-toggles">
|
||||||
<Page.Contents>
|
<Page.Contents>
|
||||||
<>
|
<>
|
||||||
{isError && getErrorMessage()}
|
{isError && getErrorMessage()}
|
||||||
{isLoading && 'Fetching feature toggles'}
|
{isLoading && 'Fetching feature toggles'}
|
||||||
{featureToggles && <AdminFeatureTogglesTable featureToggles={featureToggles} />}
|
<AlertMessage />
|
||||||
|
{featureToggles && (
|
||||||
|
<AdminFeatureTogglesTable featureToggles={featureToggles} onUpdateSuccess={handleUpdateSuccess} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
</Page.Contents>
|
</Page.Contents>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { type FeatureToggle, useUpdateFeatureTogglesMutation } from './AdminFeat
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
featureToggles: FeatureToggle[];
|
featureToggles: FeatureToggle[];
|
||||||
|
onUpdateSuccess: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortByName: SortByFn<FeatureToggle> = (a, b) => {
|
const sortByName: SortByFn<FeatureToggle> = (a, b) => {
|
||||||
@ -27,10 +28,11 @@ const sortByEnabled: SortByFn<FeatureToggle> = (a, b) => {
|
|||||||
return a.original.enabled === b.original.enabled ? 0 : a.original.enabled ? 1 : -1;
|
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<FeatureToggle[]>(featureToggles);
|
const [localToggles, setLocalToggles] = useState<FeatureToggle[]>(featureToggles);
|
||||||
const [updateFeatureToggles] = useUpdateFeatureTogglesMutation();
|
const [updateFeatureToggles] = useUpdateFeatureTogglesMutation();
|
||||||
const [modifiedToggles, setModifiedToggles] = useState<FeatureToggle[]>([]);
|
const [modifiedToggles, setModifiedToggles] = useState<FeatureToggle[]>([]);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
const handleToggleChange = (toggle: FeatureToggle, newValue: boolean) => {
|
const handleToggleChange = (toggle: FeatureToggle, newValue: boolean) => {
|
||||||
const updatedToggle = { ...toggle, enabled: newValue };
|
const updatedToggle = { ...toggle, enabled: newValue };
|
||||||
@ -56,10 +58,16 @@ export function AdminFeatureTogglesTable({ featureToggles }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveChanges = async () => {
|
const handleSaveChanges = async () => {
|
||||||
const resp = await updateFeatureToggles(modifiedToggles);
|
setIsSaving(true);
|
||||||
// Reset modifiedToggles after successful update
|
try {
|
||||||
if (!('error' in resp)) {
|
const resp = await updateFeatureToggles(modifiedToggles);
|
||||||
setModifiedToggles([]);
|
// Reset modifiedToggles after successful update
|
||||||
|
if (!('error' in resp)) {
|
||||||
|
onUpdateSuccess();
|
||||||
|
setModifiedToggles([]);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -103,8 +111,8 @@ export function AdminFeatureTogglesTable({ featureToggles }: Props) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '0 0 5px 0' }}>
|
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '0 0 5px 0' }}>
|
||||||
<Button disabled={!hasModifications()} onClick={handleSaveChanges}>
|
<Button disabled={!hasModifications() || isSaving} onClick={handleSaveChanges}>
|
||||||
Save Changes
|
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<InteractiveTable columns={columns} data={localToggles} getRowId={(featureToggle) => featureToggle.name} />
|
<InteractiveTable columns={columns} data={localToggles} getRowId={(featureToggle) => featureToggle.name} />
|
||||||
|
Reference in New Issue
Block a user