mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 02:10:45 -06:00
Feat: Feature toggle admin page frontend write UI and InteractiveTable sorting (#73533)
* updates * make save button always visible but disabled * only reset toggles if there isn't an error response * make linters happy * update post body to match backend * fix linter again * be smarter about sorting of empty descriptions * run prettier * fix payload * Re-add disabled to switch --------- Co-authored-by: Joao Calisto <joao.santana.calisto@gmail.com> Co-authored-by: Michael Mandrus <michael.mandrus@grafana.com>
This commit is contained in:
parent
025b2f3011
commit
2794b8628e
@ -102,7 +102,7 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
|
|||||||
|
|
||||||
if s.features.IsEnabled(featuremgmt.FlagFeatureToggleAdminPage) && hasAccess(ac.EvalPermission(ac.ActionFeatureManagementRead)) {
|
if s.features.IsEnabled(featuremgmt.FlagFeatureToggleAdminPage) && hasAccess(ac.EvalPermission(ac.ActionFeatureManagementRead)) {
|
||||||
configNodes = append(configNodes, &navtree.NavLink{
|
configNodes = append(configNodes, &navtree.NavLink{
|
||||||
Text: "Feature Toggles", SubTitle: "View feature toggles", Id: "feature-toggles", Url: s.cfg.AppSubURL + "/admin/featuretoggles", Icon: "toggle-on",
|
Text: "Feature Toggles", SubTitle: "View and edit feature toggles", Id: "feature-toggles", Url: s.cfg.AppSubURL + "/admin/featuretoggles", Icon: "toggle-on",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,11 +3,23 @@ import { lastValueFrom } from 'rxjs';
|
|||||||
|
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
|
type QueryArgs = {
|
||||||
|
url: string;
|
||||||
|
method?: string;
|
||||||
|
body?: { featureToggles: FeatureToggle[] };
|
||||||
|
};
|
||||||
|
|
||||||
const backendSrvBaseQuery =
|
const backendSrvBaseQuery =
|
||||||
({ baseUrl }: { baseUrl: string }): BaseQueryFn<{ url: string }> =>
|
({ baseUrl }: { baseUrl: string }): BaseQueryFn<QueryArgs> =>
|
||||||
async ({ url }) => {
|
async ({ url, method = 'GET', body }) => {
|
||||||
try {
|
try {
|
||||||
const { data } = await lastValueFrom(getBackendSrv().fetch({ url: baseUrl + url }));
|
const { data } = await lastValueFrom(
|
||||||
|
getBackendSrv().fetch({
|
||||||
|
url: baseUrl + url,
|
||||||
|
method,
|
||||||
|
data: body,
|
||||||
|
})
|
||||||
|
);
|
||||||
return { data };
|
return { data };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { error };
|
return { error };
|
||||||
@ -21,14 +33,22 @@ export const togglesApi = createApi({
|
|||||||
getFeatureToggles: builder.query<FeatureToggle[], void>({
|
getFeatureToggles: builder.query<FeatureToggle[], void>({
|
||||||
query: () => ({ url: '/featuremgmt' }),
|
query: () => ({ url: '/featuremgmt' }),
|
||||||
}),
|
}),
|
||||||
|
updateFeatureToggles: builder.mutation<void, FeatureToggle[]>({
|
||||||
|
query: (updatedToggles) => ({
|
||||||
|
url: '/featuremgmt',
|
||||||
|
method: 'POST',
|
||||||
|
body: { featureToggles: updatedToggles },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FeatureToggle = {
|
type FeatureToggle = {
|
||||||
name: string;
|
name: string;
|
||||||
|
description?: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
description: string;
|
readOnly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const { useGetFeatureTogglesQuery } = togglesApi;
|
export const { useGetFeatureTogglesQuery, useUpdateFeatureTogglesMutation } = togglesApi;
|
||||||
export type { FeatureToggle };
|
export type { FeatureToggle };
|
||||||
|
@ -1,35 +1,113 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { Switch, InteractiveTable, type CellProps } from '@grafana/ui';
|
import { Switch, InteractiveTable, type CellProps, Button, type SortByFn } from '@grafana/ui';
|
||||||
|
|
||||||
import { type FeatureToggle } from './AdminFeatureTogglesAPI';
|
import { type FeatureToggle, useUpdateFeatureTogglesMutation } from './AdminFeatureTogglesAPI';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
featureToggles: FeatureToggle[];
|
featureToggles: FeatureToggle[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sortByName: SortByFn<FeatureToggle> = (a, b) => {
|
||||||
|
return a.original.name.localeCompare(b.original.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortByDescription: SortByFn<FeatureToggle> = (a, b) => {
|
||||||
|
if (!a.original.description && !b.original.description) {
|
||||||
|
return 0;
|
||||||
|
} else if (!a.original.description) {
|
||||||
|
return 1;
|
||||||
|
} else if (!b.original.description) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return a.original.description.localeCompare(b.original.description);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortByEnabled: SortByFn<FeatureToggle> = (a, b) => {
|
||||||
|
return a.original.enabled === b.original.enabled ? 0 : a.original.enabled ? 1 : -1;
|
||||||
|
};
|
||||||
|
|
||||||
export function AdminFeatureTogglesTable({ featureToggles }: Props) {
|
export function AdminFeatureTogglesTable({ featureToggles }: Props) {
|
||||||
|
const [localToggles, setLocalToggles] = useState<FeatureToggle[]>(featureToggles);
|
||||||
|
const [updateFeatureToggles] = useUpdateFeatureTogglesMutation();
|
||||||
|
const [modifiedToggles, setModifiedToggles] = useState<FeatureToggle[]>([]);
|
||||||
|
|
||||||
|
const handleToggleChange = (toggle: FeatureToggle, newValue: boolean) => {
|
||||||
|
const updatedToggle = { ...toggle, enabled: newValue };
|
||||||
|
|
||||||
|
// 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 () => {
|
||||||
|
const resp = await updateFeatureToggles(modifiedToggles);
|
||||||
|
// Reset modifiedToggles after successful update
|
||||||
|
if (!('error' in resp)) {
|
||||||
|
setModifiedToggles([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
id: 'name',
|
id: 'name',
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
cell: ({ cell: { value } }: CellProps<FeatureToggle, string>) => <div>{value}</div>,
|
cell: ({ cell: { value } }: CellProps<FeatureToggle, string>) => <div>{value}</div>,
|
||||||
|
sortType: sortByName,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'description',
|
id: 'description',
|
||||||
header: 'Description',
|
header: 'Description',
|
||||||
cell: ({ cell: { value } }: CellProps<FeatureToggle, string>) => <div>{value}</div>,
|
cell: ({ cell: { value } }: CellProps<FeatureToggle, string>) => <div>{value}</div>,
|
||||||
|
sortType: sortByDescription,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'enabled',
|
id: 'enabled',
|
||||||
header: 'State',
|
header: 'State',
|
||||||
cell: ({ cell: { value } }: CellProps<FeatureToggle, boolean>) => (
|
cell: ({ row }: CellProps<FeatureToggle, boolean>) => (
|
||||||
<div>
|
<div>
|
||||||
<Switch value={value} disabled={true} />
|
<Switch
|
||||||
|
value={row.original.enabled}
|
||||||
|
disabled={row.original.readOnly}
|
||||||
|
onChange={(e) => handleToggleChange(row.original, e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
sortType: sortByEnabled,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return <InteractiveTable columns={columns} data={featureToggles} getRowId={(featureToggle) => featureToggle.name} />;
|
return (
|
||||||
|
<>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '0 0 5px 0' }}>
|
||||||
|
<Button disabled={!hasModifications()} onClick={handleSaveChanges}>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<InteractiveTable columns={columns} data={localToggles} getRowId={(featureToggle) => featureToggle.name} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user