diff --git a/pkg/api/api.go b/pkg/api/api.go index f35095b548e..aef223d7a8a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -111,6 +111,12 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/admin/storage", reqSignedIn, hs.Index) r.Get("/admin/storage/*", reqSignedIn, hs.Index) } + + // feature toggle admin page + if hs.Features.IsEnabled(featuremgmt.FlagFeatureToggleAdminPage) { + r.Get("/admin/featuretoggles", authorize(ac.EvalPermission(ac.ActionFeatureManagementRead)), hs.Index) + } + r.Get("/styleguide", reqSignedIn, hs.Index) r.Get("/live", reqGrafanaAdmin, hs.Index) diff --git a/pkg/services/navtree/models.go b/pkg/services/navtree/models.go index d5988a94369..8b2e7e9c746 100644 --- a/pkg/services/navtree/models.go +++ b/pkg/services/navtree/models.go @@ -131,6 +131,7 @@ func (root *NavTreeRoot) ApplyAdminIA() { adminNodeLinks = AppendIfNotNil(adminNodeLinks, root.FindById("authentication")) adminNodeLinks = AppendIfNotNil(adminNodeLinks, root.FindById("server-settings")) adminNodeLinks = AppendIfNotNil(adminNodeLinks, root.FindById("global-orgs")) + adminNodeLinks = AppendIfNotNil(adminNodeLinks, root.FindById("feature-toggles")) adminNodeLinks = AppendIfNotNil(adminNodeLinks, root.FindById("upgrading")) adminNodeLinks = AppendIfNotNil(adminNodeLinks, root.FindById("licensing")) diff --git a/pkg/services/navtree/navtreeimpl/admin.go b/pkg/services/navtree/navtreeimpl/admin.go index 3858c4e003d..db3b11d38d1 100644 --- a/pkg/services/navtree/navtreeimpl/admin.go +++ b/pkg/services/navtree/navtreeimpl/admin.go @@ -100,6 +100,12 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink }) } + if s.features.IsEnabled(featuremgmt.FlagFeatureToggleAdminPage) && hasAccess(ac.EvalPermission(ac.ActionFeatureManagementRead)) { + configNodes = append(configNodes, &navtree.NavLink{ + Text: "Feature Toggles", SubTitle: "View feature toggles", Id: "feature-toggles", Url: s.cfg.AppSubURL + "/admin/featuretoggles", Icon: "toggle-on", + }) + } + if s.features.IsEnabled(featuremgmt.FlagCorrelations) && hasAccess(correlations.ConfigurationPageAccess) { configNodes = append(configNodes, &navtree.NavLink{ Text: "Correlations", diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts index f2c436062bd..9ab94dbdf07 100644 --- a/public/app/core/reducers/root.ts +++ b/public/app/core/reducers/root.ts @@ -2,6 +2,7 @@ import { ReducersMapObject } from '@reduxjs/toolkit'; import { AnyAction, combineReducers } from 'redux'; import sharedReducers from 'app/core/reducers'; +import { togglesApi } from 'app/features/admin/AdminFeatureTogglesAPI'; import ldapReducers from 'app/features/admin/state/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; import apiKeysReducers from 'app/features/api-keys/state/reducers'; @@ -55,6 +56,7 @@ const rootReducers = { [alertingApi.reducerPath]: alertingApi.reducer, [publicDashboardApi.reducerPath]: publicDashboardApi.reducer, [browseDashboardsAPI.reducerPath]: browseDashboardsAPI.reducer, + [togglesApi.reducerPath]: togglesApi.reducer, }; const addedReducers = {}; diff --git a/public/app/features/admin/AdminFeatureTogglesAPI.ts b/public/app/features/admin/AdminFeatureTogglesAPI.ts new file mode 100644 index 00000000000..47857e68ba8 --- /dev/null +++ b/public/app/features/admin/AdminFeatureTogglesAPI.ts @@ -0,0 +1,34 @@ +import { createApi, BaseQueryFn } from '@reduxjs/toolkit/query/react'; +import { lastValueFrom } from 'rxjs'; + +import { getBackendSrv } from '@grafana/runtime'; + +const backendSrvBaseQuery = + ({ baseUrl }: { baseUrl: string }): BaseQueryFn<{ url: string }> => + async ({ url }) => { + try { + const { data } = await lastValueFrom(getBackendSrv().fetch({ url: baseUrl + url })); + return { data }; + } catch (error) { + return { error }; + } + }; + +export const togglesApi = createApi({ + reducerPath: 'togglesApi', + baseQuery: backendSrvBaseQuery({ baseUrl: '/api' }), + endpoints: (builder) => ({ + getFeatureToggles: builder.query({ + query: () => ({ url: '/featuremgmt' }), + }), + }), +}); + +type FeatureToggle = { + name: string; + enabled: boolean; + description: string; +}; + +export const { useGetFeatureTogglesQuery } = togglesApi; +export type { FeatureToggle }; diff --git a/public/app/features/admin/AdminFeatureTogglesPage.tsx b/public/app/features/admin/AdminFeatureTogglesPage.tsx new file mode 100644 index 00000000000..36bdfca4f2c --- /dev/null +++ b/public/app/features/admin/AdminFeatureTogglesPage.tsx @@ -0,0 +1,26 @@ +import React from 'react'; + +import { Page } from 'app/core/components/Page/Page'; + +import { useGetFeatureTogglesQuery } from './AdminFeatureTogglesAPI'; +import { AdminFeatureTogglesTable } from './AdminFeatureTogglesTable'; + +export default function AdminFeatureTogglesPage() { + const { data: featureToggles, isLoading, isError } = useGetFeatureTogglesQuery(); + + const getErrorMessage = () => { + return 'Error fetching feature toggles'; + }; + + return ( + + + <> + {isError && getErrorMessage()} + {isLoading && 'Fetching feature toggles'} + {featureToggles && } + + + + ); +} diff --git a/public/app/features/admin/AdminFeatureTogglesTable.tsx b/public/app/features/admin/AdminFeatureTogglesTable.tsx new file mode 100644 index 00000000000..aaa37ab67ae --- /dev/null +++ b/public/app/features/admin/AdminFeatureTogglesTable.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import { Switch, InteractiveTable, type CellProps } from '@grafana/ui'; + +import { type FeatureToggle } from './AdminFeatureTogglesAPI'; + +interface Props { + featureToggles: FeatureToggle[]; +} + +export function AdminFeatureTogglesTable({ featureToggles }: Props) { + const columns = [ + { + id: 'name', + header: 'Name', + cell: ({ cell: { value } }: CellProps) =>
{value}
, + }, + { + id: 'description', + header: 'Description', + cell: ({ cell: { value } }: CellProps) =>
{value}
, + }, + { + id: 'enabled', + header: 'State', + cell: ({ cell: { value } }: CellProps) => ( +
+ +
+ ), + }, + ]; + + return featureToggle.name} />; +} diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 16f202022ea..2a7fecc24fc 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -357,6 +357,14 @@ export function getAppRoutes(): RouteDescriptor[] { () => import(/* webpackChunkName: "AdminEditOrgPage" */ 'app/features/admin/AdminEditOrgPage') ), }, + { + path: '/admin/featuretoggles', + component: config.featureToggles.featureToggleAdminPage + ? SafeDynamicImport( + () => import(/* webpackChunkName: "AdminFeatureTogglesPage" */ 'app/features/admin/AdminFeatureTogglesPage') + ) + : () => , + }, { path: '/admin/storage/:path*', roles: () => ['Admin'], diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index a7a462bf3a8..c2c6cc89625 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -1,6 +1,7 @@ import { configureStore as reduxConfigureStore, createListenerMiddleware } from '@reduxjs/toolkit'; import { setupListeners } from '@reduxjs/toolkit/query'; +import { togglesApi } from 'app/features/admin/AdminFeatureTogglesAPI'; import { browseDashboardsAPI } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { publicDashboardApi } from 'app/features/dashboard/api/publicDashboardApi'; import { StoreState } from 'app/types/store'; @@ -28,7 +29,8 @@ export function configureStore(initialState?: Partial) { listenerMiddleware.middleware, alertingApi.middleware, publicDashboardApi.middleware, - browseDashboardsAPI.middleware + browseDashboardsAPI.middleware, + togglesApi.middleware ), devTools: process.env.NODE_ENV !== 'production', preloadedState: {