mirror of
https://github.com/grafana/grafana.git
synced 2025-02-14 01:23:32 -06:00
Feature Management: UI improvements (#76866)
* Feature Management: UI improvements * update UI logic --------- Co-authored-by: Michael Mandrus <michael.mandrus@grafana.com>
This commit is contained in:
parent
c4bf32fa2d
commit
7869ca1932
@ -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)
|
||||
|
@ -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() {
|
||||
|
@ -132,4 +132,5 @@ type FeatureToggleDTO struct {
|
||||
|
||||
type FeatureManagerState struct {
|
||||
RestartRequired bool `json:"restartRequired"`
|
||||
AllowEditing bool `json:"allowEditing"`
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -55,6 +55,7 @@ type FeatureToggle = {
|
||||
|
||||
type FeatureMgmtState = {
|
||||
restartRequired: boolean;
|
||||
allowEditing: boolean;
|
||||
};
|
||||
|
||||
export const { useGetManagerStateQuery, useGetFeatureTogglesQuery, useUpdateFeatureTogglesMutation } = togglesApi;
|
||||
|
@ -23,7 +23,7 @@ export default function AdminFeatureTogglesPage() {
|
||||
setUpdateSuccessful(true);
|
||||
};
|
||||
|
||||
const AlertMessage = () => {
|
||||
const EditingAlert = () => {
|
||||
return (
|
||||
<div className={styles.warning}>
|
||||
<div className={styles.icon}>
|
||||
@ -44,9 +44,13 @@ export default function AdminFeatureTogglesPage() {
|
||||
<>
|
||||
{isError && getErrorMessage()}
|
||||
{isLoading && 'Fetching feature toggles'}
|
||||
<AlertMessage />
|
||||
{featureMgmtState?.allowEditing && <EditingAlert />}
|
||||
{featureToggles && (
|
||||
<AdminFeatureTogglesTable featureToggles={featureToggles} onUpdateSuccess={handleUpdateSuccess} />
|
||||
<AdminFeatureTogglesTable
|
||||
featureToggles={featureToggles}
|
||||
allowEditing={featureMgmtState?.allowEditing || false}
|
||||
onUpdateSuccess={handleUpdateSuccess}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Page.Contents>
|
||||
@ -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,
|
||||
|
@ -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<FeatureToggle> = (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<FeatureToggle[]>(featureToggles);
|
||||
const [localToggles, setLocalToggles] = useState<FeatureToggle[]>(featureToggles);
|
||||
const [updateFeatureToggles] = useUpdateFeatureTogglesMutation();
|
||||
const [modifiedToggles, setModifiedToggles] = useState<FeatureToggle[]>([]);
|
||||
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<FeatureToggle, boolean>) => (
|
||||
<div>
|
||||
<Switch
|
||||
value={row.original.enabled}
|
||||
disabled={row.original.readOnly}
|
||||
onChange={(e) => handleToggleChange(row.original, e.currentTarget.checked)}
|
||||
/>
|
||||
</div>
|
||||
<Tooltip content={getToggleTooltipContent(row.original.readOnly)}>
|
||||
<div>
|
||||
<Switch
|
||||
value={row.original.enabled}
|
||||
disabled={row.original.readOnly}
|
||||
onChange={(e) => handleToggleChange(row.original, e.currentTarget.checked)}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
),
|
||||
sortType: sortByEnabled,
|
||||
},
|
||||
@ -110,11 +110,13 @@ export function AdminFeatureTogglesTable({ featureToggles, onUpdateSuccess }: Pr
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '0 0 5px 0' }}>
|
||||
<Button disabled={!hasModifications() || isSaving} onClick={handleSaveChanges}>
|
||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</div>
|
||||
{allowEditing && (
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '0 0 5px 0' }}>
|
||||
<Button disabled={!hasModifications() || isSaving} onClick={handleSaveChanges}>
|
||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<InteractiveTable columns={columns} data={localToggles} getRowId={(featureToggle) => featureToggle.name} />
|
||||
</>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user