diff --git a/public/app/core/components/AccessControl/Permissions.tsx b/public/app/core/components/AccessControl/Permissions.tsx
index 2a203e0a1f5..68c147d5cac 100644
--- a/public/app/core/components/AccessControl/Permissions.tsx
+++ b/public/app/core/components/AccessControl/Permissions.tsx
@@ -133,39 +133,41 @@ export const Permissions = ({
return (
- {config.featureToggles.nestedFolders && resource === 'folders' && (
+ {canSetPermissions && (
<>
- This will change permissions for this folder and all its descendants. In total, this will affect:
-
-
+ {config.featureToggles.nestedFolders && resource === 'folders' && (
+ <>
+ This will change permissions for this folder and all its descendants. In total, this will affect:
+
+
+ >
+ )}
+
setIsAdding(true)}
+ >
+ {buttonLabel}
+
+
+ setIsAdding(false)}
+ />
+
>
)}
- {canSetPermissions && (
-
setIsAdding(true)}
- >
- {buttonLabel}
-
- )}
-
- setIsAdding(false)}
- />
-
{items.length === 0 && (
diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx
index 3b639582149..40562721011 100644
--- a/public/app/core/components/AppChrome/AppChromeService.tsx
+++ b/public/app/core/components/AppChrome/AppChromeService.tsx
@@ -165,7 +165,7 @@ export class AppChromeService {
}
/**
- * Checks if text, url and active child url are the same
+ * Checks if text, url, active child url and parent are the same
**/
function navItemsAreTheSame(a: NavModelItem | undefined, b: NavModelItem | undefined) {
if (a === b) {
@@ -175,5 +175,10 @@ function navItemsAreTheSame(a: NavModelItem | undefined, b: NavModelItem | undef
const aActiveChild = a?.children?.find((child) => child.active);
const bActiveChild = b?.children?.find((child) => child.active);
- return a?.text === b?.text && a?.url === b?.url && aActiveChild?.url === bActiveChild?.url;
+ return (
+ a?.text === b?.text &&
+ a?.url === b?.url &&
+ aActiveChild?.url === bActiveChild?.url &&
+ a?.parentItem === b?.parentItem
+ );
}
diff --git a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx
new file mode 100644
index 00000000000..956cf12bf10
--- /dev/null
+++ b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx
@@ -0,0 +1,112 @@
+import 'whatwg-fetch'; // fetch polyfill
+import { render as rtlRender, screen } from '@testing-library/react';
+import { rest } from 'msw';
+import { SetupServer, setupServer } from 'msw/node';
+import React from 'react';
+import { TestProvider } from 'test/helpers/TestProvider';
+
+import { contextSrv } from 'app/core/core';
+import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
+import { backendSrv } from 'app/core/services/backend_srv';
+
+import BrowseFolderAlertingPage, { OwnProps } from './BrowseFolderAlertingPage';
+import { getPrometheusRulesResponse, getRulerRulesResponse } from './fixtures/alertRules.fixture';
+
+function render(...[ui, options]: Parameters) {
+ rtlRender({ui} , options);
+}
+
+jest.mock('@grafana/runtime', () => ({
+ ...jest.requireActual('@grafana/runtime'),
+ getBackendSrv: () => backendSrv,
+ config: {
+ ...jest.requireActual('@grafana/runtime').config,
+ unifiedAlertingEnabled: true,
+ },
+}));
+
+const mockFolderName = 'myFolder';
+const mockFolderUid = '12345';
+
+const mockRulerRulesResponse = getRulerRulesResponse(mockFolderName, mockFolderUid);
+const mockPrometheusRulesResponse = getPrometheusRulesResponse(mockFolderName);
+
+describe('browse-dashboards BrowseFolderAlertingPage', () => {
+ let props: OwnProps;
+ let server: SetupServer;
+
+ beforeAll(() => {
+ server = setupServer(
+ rest.get('/api/folders/:uid', (_, res, ctx) => {
+ return res(
+ ctx.status(200),
+ ctx.json({
+ title: mockFolderName,
+ uid: mockFolderUid,
+ })
+ );
+ }),
+ rest.get('api/ruler/grafana/api/v1/rules', (_, res, ctx) => {
+ return res(ctx.status(200), ctx.json(mockRulerRulesResponse));
+ }),
+ rest.get('api/prometheus/grafana/api/v1/rules', (_, res, ctx) => {
+ return res(ctx.status(200), ctx.json(mockPrometheusRulesResponse));
+ })
+ );
+ server.listen();
+ });
+
+ afterAll(() => {
+ server.close();
+ });
+
+ beforeEach(() => {
+ jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
+ props = {
+ ...getRouteComponentProps({
+ match: {
+ params: {
+ uid: mockFolderUid,
+ },
+ isExact: false,
+ path: '',
+ url: '',
+ },
+ }),
+ };
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ server.resetHandlers();
+ });
+
+ it('displays the folder title', async () => {
+ render( );
+ expect(await screen.findByRole('heading', { name: mockFolderName })).toBeInTheDocument();
+ });
+
+ it('displays the "Folder actions" button', async () => {
+ render( );
+ expect(await screen.findByRole('button', { name: 'Folder actions' })).toBeInTheDocument();
+ });
+
+ it('displays all the folder tabs and shows the "Alert rules" tab as selected', async () => {
+ render( );
+ expect(await screen.findByRole('tab', { name: 'Tab Dashboards' })).toBeInTheDocument();
+ expect(await screen.findByRole('tab', { name: 'Tab Dashboards' })).toHaveAttribute('aria-selected', 'false');
+
+ expect(await screen.findByRole('tab', { name: 'Tab Panels' })).toBeInTheDocument();
+ expect(await screen.findByRole('tab', { name: 'Tab Panels' })).toHaveAttribute('aria-selected', 'false');
+
+ expect(await screen.findByRole('tab', { name: 'Tab Alert rules' })).toBeInTheDocument();
+ expect(await screen.findByRole('tab', { name: 'Tab Alert rules' })).toHaveAttribute('aria-selected', 'true');
+ });
+
+ it('displays the alert rules returned by the API', async () => {
+ render( );
+
+ const ruleName = mockPrometheusRulesResponse.data.groups[0].rules[0].name;
+ expect(await screen.findByRole('link', { name: ruleName })).toBeInTheDocument();
+ });
+});
diff --git a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.tsx b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.tsx
new file mode 100644
index 00000000000..b4124d61f85
--- /dev/null
+++ b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.tsx
@@ -0,0 +1,48 @@
+import React, { useMemo } from 'react';
+
+import { Page } from 'app/core/components/Page/Page';
+import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
+import { buildNavModel, getAlertingTabID } from 'app/features/folders/state/navModel';
+import { useSelector } from 'app/types';
+
+import { AlertsFolderView } from '../alerting/unified/AlertsFolderView';
+
+import { useGetFolderQuery } from './api/browseDashboardsAPI';
+import { FolderActionsButton } from './components/FolderActionsButton';
+
+export interface OwnProps extends GrafanaRouteComponentProps<{ uid: string }> {}
+
+export function BrowseFolderAlertingPage({ match }: OwnProps) {
+ const { uid: folderUID } = match.params;
+ const { data: folderDTO, isLoading } = useGetFolderQuery(folderUID);
+ const folder = useSelector((state) => state.folder);
+
+ const navModel = useMemo(() => {
+ if (!folderDTO) {
+ return undefined;
+ }
+ const model = buildNavModel(folderDTO);
+
+ // Set the "Alerting" tab to active
+ const alertingTabID = getAlertingTabID(folderDTO.uid);
+ const alertingTab = model.children?.find((child) => child.id === alertingTabID);
+ if (alertingTab) {
+ alertingTab.active = true;
+ }
+ return model;
+ }, [folderDTO]);
+
+ return (
+ {folderDTO && }>}
+ >
+
+
+
+
+ );
+}
+
+export default BrowseFolderAlertingPage;
diff --git a/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx b/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx
new file mode 100644
index 00000000000..6409a844921
--- /dev/null
+++ b/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx
@@ -0,0 +1,116 @@
+import 'whatwg-fetch'; // fetch polyfill
+import { render as rtlRender, screen } from '@testing-library/react';
+import { rest } from 'msw';
+import { SetupServer, setupServer } from 'msw/node';
+import React from 'react';
+import { TestProvider } from 'test/helpers/TestProvider';
+
+import { contextSrv } from 'app/core/core';
+import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
+import { backendSrv } from 'app/core/services/backend_srv';
+
+import BrowseFolderLibraryPanelsPage, { OwnProps } from './BrowseFolderLibraryPanelsPage';
+import { getLibraryElementsResponse } from './fixtures/libraryElements.fixture';
+
+function render(...[ui, options]: Parameters) {
+ rtlRender({ui} , options);
+}
+
+jest.mock('@grafana/runtime', () => ({
+ ...jest.requireActual('@grafana/runtime'),
+ getBackendSrv: () => backendSrv,
+ config: {
+ ...jest.requireActual('@grafana/runtime').config,
+ unifiedAlertingEnabled: true,
+ },
+}));
+
+const mockFolderName = 'myFolder';
+const mockFolderUid = '12345';
+const mockLibraryElementsResponse = getLibraryElementsResponse(1, {
+ folderUid: mockFolderUid,
+});
+
+describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => {
+ let props: OwnProps;
+ let server: SetupServer;
+
+ beforeAll(() => {
+ server = setupServer(
+ rest.get('/api/folders/:uid', (_, res, ctx) => {
+ return res(
+ ctx.status(200),
+ ctx.json({
+ title: mockFolderName,
+ uid: mockFolderUid,
+ })
+ );
+ }),
+ rest.get('/api/library-elements', (_, res, ctx) => {
+ return res(
+ ctx.status(200),
+ ctx.json({
+ result: mockLibraryElementsResponse,
+ })
+ );
+ }),
+ rest.get('/api/search/sorting', (_, res, ctx) => {
+ return res(ctx.status(200), ctx.json({}));
+ })
+ );
+ server.listen();
+ });
+
+ afterAll(() => {
+ server.close();
+ });
+
+ beforeEach(() => {
+ jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
+ props = {
+ ...getRouteComponentProps({
+ match: {
+ params: {
+ uid: mockFolderUid,
+ },
+ isExact: false,
+ path: '',
+ url: '',
+ },
+ }),
+ };
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ server.resetHandlers();
+ });
+
+ it('displays the folder title', async () => {
+ render( );
+ expect(await screen.findByRole('heading', { name: mockFolderName })).toBeInTheDocument();
+ });
+
+ it('displays the "Folder actions" button', async () => {
+ render( );
+ expect(await screen.findByRole('button', { name: 'Folder actions' })).toBeInTheDocument();
+ });
+
+ it('displays all the folder tabs and shows the "Library panels" tab as selected', async () => {
+ render( );
+ expect(await screen.findByRole('tab', { name: 'Tab Dashboards' })).toBeInTheDocument();
+ expect(await screen.findByRole('tab', { name: 'Tab Dashboards' })).toHaveAttribute('aria-selected', 'false');
+
+ expect(await screen.findByRole('tab', { name: 'Tab Panels' })).toBeInTheDocument();
+ expect(await screen.findByRole('tab', { name: 'Tab Panels' })).toHaveAttribute('aria-selected', 'true');
+
+ expect(await screen.findByRole('tab', { name: 'Tab Alert rules' })).toBeInTheDocument();
+ expect(await screen.findByRole('tab', { name: 'Tab Alert rules' })).toHaveAttribute('aria-selected', 'false');
+ });
+
+ it('displays the library panels returned by the API', async () => {
+ render( );
+
+ expect(await screen.findByText(mockLibraryElementsResponse.elements[0].name)).toBeInTheDocument();
+ });
+});
diff --git a/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.tsx b/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.tsx
new file mode 100644
index 00000000000..45e0e269cf4
--- /dev/null
+++ b/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.tsx
@@ -0,0 +1,56 @@
+import React, { useMemo, useState } from 'react';
+
+import { Page } from 'app/core/components/Page/Page';
+
+import { GrafanaRouteComponentProps } from '../../core/navigation/types';
+import { FolderActionsButton } from '../browse-dashboards/components/FolderActionsButton';
+import { buildNavModel, getLibraryPanelsTabID } from '../folders/state/navModel';
+import { LibraryPanelsSearch } from '../library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch';
+import { OpenLibraryPanelModal } from '../library-panels/components/OpenLibraryPanelModal/OpenLibraryPanelModal';
+import { LibraryElementDTO } from '../library-panels/types';
+
+import { useGetFolderQuery } from './api/browseDashboardsAPI';
+
+export interface OwnProps extends GrafanaRouteComponentProps<{ uid: string }> {}
+
+export function BrowseFolderLibraryPanelsPage({ match }: OwnProps) {
+ const { uid: folderUID } = match.params;
+ const { data: folderDTO, isLoading } = useGetFolderQuery(folderUID);
+ const [selected, setSelected] = useState(undefined);
+
+ const navModel = useMemo(() => {
+ if (!folderDTO) {
+ return undefined;
+ }
+ const model = buildNavModel(folderDTO);
+
+ // Set the "Library panels" tab to active
+ const libraryPanelsTabID = getLibraryPanelsTabID(folderDTO.uid);
+ const libraryPanelsTab = model.children?.find((child) => child.id === libraryPanelsTabID);
+ if (libraryPanelsTab) {
+ libraryPanelsTab.active = true;
+ }
+ return model;
+ }, [folderDTO]);
+
+ return (
+ {folderDTO && }>}
+ >
+
+
+ {selected ? setSelected(undefined)} libraryPanel={selected} /> : null}
+
+
+ );
+}
+
+export default BrowseFolderLibraryPanelsPage;
diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts
index 8b46af6b365..b156f001155 100644
--- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts
+++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts
@@ -32,11 +32,13 @@ function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryF
}
export const browseDashboardsAPI = createApi({
+ tagTypes: ['getFolder'],
reducerPath: 'browseDashboardsAPI',
baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }),
endpoints: (builder) => ({
getFolder: builder.query({
query: (folderUID) => ({ url: `/folders/${folderUID}`, params: { accesscontrol: true } }),
+ providesTags: (_result, _error, arg) => [{ type: 'getFolder', id: arg }],
}),
getAffectedItems: builder.query({
queryFn: async (selectedItems) => {
@@ -67,8 +69,16 @@ export const browseDashboardsAPI = createApi({
return { data: totalCounts };
},
}),
+ moveFolder: builder.mutation({
+ query: ({ folderUID, destinationUID }) => ({
+ url: `/folders/${folderUID}/move`,
+ method: 'POST',
+ data: { parentUID: destinationUID },
+ }),
+ invalidatesTags: (_result, _error, arg) => [{ type: 'getFolder', id: arg.folderUID }],
+ }),
}),
});
-export const { useGetAffectedItemsQuery, useGetFolderQuery } = browseDashboardsAPI;
+export const { endpoints, useGetAffectedItemsQuery, useGetFolderQuery, useMoveFolderMutation } = browseDashboardsAPI;
export { skipToken } from '@reduxjs/toolkit/query/react';
diff --git a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx
index 07b607d1c7f..72e69e98e32 100644
--- a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx
+++ b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx
@@ -8,13 +8,13 @@ import { useSearchStateManager } from 'app/features/search/state/SearchStateMana
import { useDispatch, useSelector } from 'app/types';
import { ShowModalReactEvent } from 'app/types/events';
+import { useMoveFolderMutation } from '../../api/browseDashboardsAPI';
import {
childrenByParentUIDSelector,
deleteDashboard,
deleteFolder,
fetchChildren,
moveDashboard,
- moveFolder,
rootItemsSelector,
setAllSelection,
useActionSelectionState,
@@ -33,6 +33,7 @@ export function BrowseActions() {
const selectedDashboards = Object.keys(selectedItems.dashboard).filter((uid) => selectedItems.dashboard[uid]);
const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
const rootItems = useSelector(rootItemsSelector);
+ const [moveFolder] = useMoveFolderMutation();
const childrenByParentUID = useSelector(childrenByParentUIDSelector);
const [, stateManager] = useSearchStateManager();
const isSearching = stateManager.hasSearchFilters();
@@ -84,7 +85,7 @@ export function BrowseActions() {
// Move all the folders sequentially
// TODO error handling here
for (const folderUID of selectedFolders) {
- await dispatch(moveFolder({ folderUID, destinationUID }));
+ await moveFolder({ folderUID, destinationUID });
// find the parent folder uid and add it to parentsToRefresh
const folder = findItem(rootItems ?? [], childrenByParentUID, folderUID);
parentsToRefresh.add(folder?.parentUID);
diff --git a/public/app/features/browse-dashboards/components/CreateNewButton.tsx b/public/app/features/browse-dashboards/components/CreateNewButton.tsx
index ac38ca4945f..c1757a4a744 100644
--- a/public/app/features/browse-dashboards/components/CreateNewButton.tsx
+++ b/public/app/features/browse-dashboards/components/CreateNewButton.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useState } from 'react';
import { Button, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';
import {
@@ -18,6 +18,7 @@ interface Props {
}
export function CreateNewButton({ inFolder, canCreateDashboard, canCreateFolder }: Props) {
+ const [isOpen, setIsOpen] = useState(false);
const newMenu = (
{canCreateDashboard && (
@@ -33,10 +34,10 @@ export function CreateNewButton({ inFolder, canCreateDashboard, canCreateFolder
);
return (
-
+
{getNewPhrase()}
-
+
);
diff --git a/public/app/features/browse-dashboards/components/FolderActionsButton.tsx b/public/app/features/browse-dashboards/components/FolderActionsButton.tsx
index cd60edbb469..ecbedfd7401 100644
--- a/public/app/features/browse-dashboards/components/FolderActionsButton.tsx
+++ b/public/app/features/browse-dashboards/components/FolderActionsButton.tsx
@@ -1,29 +1,100 @@
import React, { useState } from 'react';
+import { locationService } from '@grafana/runtime';
import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';
import { Permissions } from 'app/core/components/AccessControl';
-import { contextSrv } from 'app/core/core';
-import { AccessControlAction, FolderDTO } from 'app/types';
+import { appEvents, contextSrv } from 'app/core/core';
+import { AccessControlAction, FolderDTO, useDispatch } from 'app/types';
+import { ShowModalReactEvent } from 'app/types/events';
+
+import { useMoveFolderMutation } from '../api/browseDashboardsAPI';
+import { deleteFolder, fetchChildren } from '../state';
+
+import { DeleteModal } from './BrowseActions/DeleteModal';
+import { MoveModal } from './BrowseActions/MoveModal';
interface Props {
folder: FolderDTO;
}
export function FolderActionsButton({ folder }: Props) {
+ const dispatch = useDispatch();
+ const [isOpen, setIsOpen] = useState(false);
const [showPermissionsDrawer, setShowPermissionsDrawer] = useState(false);
+ const [moveFolder] = useMoveFolderMutation();
+ const canViewPermissions = contextSrv.hasPermission(AccessControlAction.FoldersPermissionsRead);
const canSetPermissions = contextSrv.hasPermission(AccessControlAction.FoldersPermissionsWrite);
+ const canMoveFolder = contextSrv.hasPermission(AccessControlAction.FoldersWrite);
+ const canDeleteFolder = contextSrv.hasPermission(AccessControlAction.FoldersDelete);
+
+ const onMove = async (destinationUID: string) => {
+ await moveFolder({ folderUID: folder.uid, destinationUID });
+ dispatch(fetchChildren(destinationUID));
+ if (folder.parentUid) {
+ dispatch(fetchChildren(folder.parentUid));
+ }
+ };
+
+ const onDelete = async () => {
+ await dispatch(deleteFolder(folder.uid));
+ if (folder.parentUid) {
+ dispatch(fetchChildren(folder.parentUid));
+ }
+ locationService.push('/dashboards');
+ };
+
+ const showMoveModal = () => {
+ appEvents.publish(
+ new ShowModalReactEvent({
+ component: MoveModal,
+ props: {
+ selectedItems: {
+ folder: { [folder.uid]: true },
+ dashboard: {},
+ panel: {},
+ $all: false,
+ },
+ onConfirm: onMove,
+ },
+ })
+ );
+ };
+
+ const showDeleteModal = () => {
+ appEvents.publish(
+ new ShowModalReactEvent({
+ component: DeleteModal,
+ props: {
+ selectedItems: {
+ folder: { [folder.uid]: true },
+ dashboard: {},
+ panel: {},
+ $all: false,
+ },
+ onConfirm: onDelete,
+ },
+ })
+ );
+ };
+
const menu = (
- setShowPermissionsDrawer(true)} label="Set permissions" />
+ {canViewPermissions && setShowPermissionsDrawer(true)} label="Manage permissions" />}
+ {canMoveFolder && }
+ {canDeleteFolder && }
);
+ if (!canViewPermissions && !canMoveFolder && !canDeleteFolder) {
+ return null;
+ }
+
return (
<>
-
+
Folder actions
-
+
{showPermissionsDrawer && (
diff --git a/public/app/features/browse-dashboards/fixtures/alertRules.fixture.ts b/public/app/features/browse-dashboards/fixtures/alertRules.fixture.ts
new file mode 100644
index 00000000000..f3d9d146cbe
--- /dev/null
+++ b/public/app/features/browse-dashboards/fixtures/alertRules.fixture.ts
@@ -0,0 +1,93 @@
+import { Chance } from 'chance';
+
+import {
+ GrafanaAlertStateDecision,
+ PromAlertingRuleState,
+ PromRulesResponse,
+ PromRuleType,
+ RulerRulesConfigDTO,
+} from 'app/types/unified-alerting-dto';
+
+export function getRulerRulesResponse(folderName: string, folderUid: string, seed = 1): RulerRulesConfigDTO {
+ const random = Chance(seed);
+ return {
+ [folderName]: [
+ {
+ name: 'foo',
+ interval: '1m',
+ rules: [
+ {
+ annotations: {},
+ labels: {},
+ expr: '',
+ for: '5m',
+ grafana_alert: {
+ id: '49',
+ title: random.sentence({ words: 3 }),
+ condition: 'B',
+ data: [
+ {
+ refId: 'A',
+ queryType: '',
+ relativeTimeRange: {
+ from: 600,
+ to: 0,
+ },
+ datasourceUid: 'gdev-testdata',
+ model: {
+ hide: false,
+ intervalMs: 1000,
+ maxDataPoints: 43200,
+ refId: 'A',
+ },
+ },
+ ],
+ uid: random.guid(),
+ namespace_uid: folderUid,
+ namespace_id: 0,
+ no_data_state: GrafanaAlertStateDecision.NoData,
+ exec_err_state: GrafanaAlertStateDecision.Error,
+ is_paused: false,
+ },
+ },
+ ],
+ },
+ ],
+ };
+}
+
+export function getPrometheusRulesResponse(folderName: string, seed = 1): PromRulesResponse {
+ const random = Chance(seed);
+ return {
+ status: 'success',
+ data: {
+ groups: [
+ {
+ name: 'foo',
+ file: folderName,
+ rules: [
+ {
+ alerts: [],
+ labels: {},
+ state: PromAlertingRuleState.Inactive,
+ name: random.sentence({ words: 3 }),
+ query:
+ '[{"refId":"A","queryType":"","relativeTimeRange":{"from":600,"to":0},"datasourceUid":"gdev-testdata","model":{"hide":false,"intervalMs":1000,"maxDataPoints":43200,"refId":"A"}},{"refId":"B","queryType":"","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"__expr__","model":{"conditions":[{"evaluator":{"params":[0,0],"type":"gt"},"operator":{"type":"and"},"query":{"params":[]},"reducer":{"params":[],"type":"avg"},"type":"query"}],"datasource":{"name":"Expression","type":"__expr__","uid":"__expr__"},"expression":"A","intervalMs":1000,"maxDataPoints":43200,"refId":"B","type":"threshold"}}]',
+ duration: 300,
+ health: 'ok',
+ type: PromRuleType.Alerting,
+ lastEvaluation: '0001-01-01T00:00:00Z',
+ evaluationTime: 0,
+ },
+ ],
+ interval: 60,
+ lastEvaluation: '0001-01-01T00:00:00Z',
+ evaluationTime: 0,
+ },
+ ],
+ totals: {
+ inactive: 1,
+ },
+ },
+ };
+}
diff --git a/public/app/features/browse-dashboards/fixtures/libraryElements.fixture.ts b/public/app/features/browse-dashboards/fixtures/libraryElements.fixture.ts
new file mode 100644
index 00000000000..f166b442c47
--- /dev/null
+++ b/public/app/features/browse-dashboards/fixtures/libraryElements.fixture.ts
@@ -0,0 +1,38 @@
+import { Chance } from 'chance';
+
+import { LibraryPanel } from '@grafana/schema';
+
+import { LibraryElementsSearchResult } from '../../library-panels/types';
+
+export function getLibraryElementsResponse(length = 1, overrides?: Partial): LibraryElementsSearchResult {
+ const elements: LibraryPanel[] = [];
+ for (let i = 0; i < length; i++) {
+ const random = Chance(i);
+ const libraryElement: LibraryPanel = {
+ type: 'timeseries',
+ uid: random.guid(),
+ version: 1,
+ name: random.sentence({ words: 3 }),
+ folderUid: random.guid(),
+ model: {
+ type: 'timeseries',
+ fieldConfig: {
+ defaults: {},
+ overrides: [],
+ },
+ options: {},
+ repeatDirection: 'h',
+ transformations: [],
+ transparent: false,
+ },
+ ...overrides,
+ };
+ elements.push(libraryElement);
+ }
+ return {
+ page: 1,
+ perPage: 40,
+ totalCount: elements.length,
+ elements,
+ };
+}
diff --git a/public/app/features/browse-dashboards/state/actions.ts b/public/app/features/browse-dashboards/state/actions.ts
index 8ee42212b5d..c6d3aa8c4e7 100644
--- a/public/app/features/browse-dashboards/state/actions.ts
+++ b/public/app/features/browse-dashboards/state/actions.ts
@@ -42,10 +42,3 @@ export const moveDashboard = createAsyncThunk(
});
}
);
-
-export const moveFolder = createAsyncThunk(
- 'browseDashboards/moveFolder',
- async ({ folderUID, destinationUID }: { folderUID: string; destinationUID: string }) => {
- return getBackendSrv().post(`/api/folders/${folderUID}/move`, { parentUID: destinationUID });
- }
-);
diff --git a/public/app/features/folders/state/reducers.ts b/public/app/features/folders/state/reducers.ts
index 247fb5a84a0..46bf07ec855 100644
--- a/public/app/features/folders/state/reducers.ts
+++ b/public/app/features/folders/state/reducers.ts
@@ -1,6 +1,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { processAclItems } from 'app/core/utils/acl';
+import { endpoints } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
import { DashboardAclDTO, FolderDTO, FolderState } from 'app/types';
export const initialState: FolderState = {
@@ -16,17 +17,19 @@ export const initialState: FolderState = {
canViewFolderPermissions: false,
};
+const loadFolderReducer = (state: FolderState, action: PayloadAction): FolderState => {
+ return {
+ ...state,
+ ...action.payload,
+ hasChanged: false,
+ };
+};
+
const folderSlice = createSlice({
name: 'folder',
initialState,
reducers: {
- loadFolder: (state, action: PayloadAction): FolderState => {
- return {
- ...state,
- ...action.payload,
- hasChanged: false,
- };
- },
+ loadFolder: loadFolderReducer,
setFolderTitle: (state, action: PayloadAction): FolderState => {
return {
...state,
@@ -45,6 +48,9 @@ const folderSlice = createSlice({
return state;
},
},
+ extraReducers: (builder) => {
+ builder.addMatcher(endpoints.getFolder.matchFulfilled, loadFolderReducer);
+ },
});
export const { loadFolderPermissions, loadFolder, setFolderTitle, setCanViewFolderPermissions } = folderSlice.actions;
diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx
index b683dd16ccb..991b59693b4 100644
--- a/public/app/routes/routes.tsx
+++ b/public/app/routes/routes.tsx
@@ -477,7 +477,13 @@ export function getAppRoutes(): RouteDescriptor[] {
{
path: '/dashboards/f/:uid/:slug/library-panels',
component: SafeDynamicImport(
- () => import(/* webpackChunkName: "FolderLibraryPanelsPage"*/ 'app/features/folders/FolderLibraryPanelsPage')
+ config.featureToggles.nestedFolders
+ ? () =>
+ import(
+ /* webpackChunkName: "FolderLibraryPanelsPage"*/ 'app/features/browse-dashboards/BrowseFolderLibraryPanelsPage'
+ )
+ : () =>
+ import(/* webpackChunkName: "FolderLibraryPanelsPage"*/ 'app/features/folders/FolderLibraryPanelsPage')
),
},
{
@@ -485,7 +491,10 @@ export function getAppRoutes(): RouteDescriptor[] {
roles: () =>
contextSrv.evaluatePermission(() => ['Viewer', 'Editor', 'Admin'], [AccessControlAction.AlertingRuleRead]),
component: SafeDynamicImport(
- () => import(/* webpackChunkName: "FolderAlerting"*/ 'app/features/folders/FolderAlerting')
+ config.featureToggles.nestedFolders
+ ? () =>
+ import(/* webpackChunkName: "FolderAlerting"*/ 'app/features/browse-dashboards/BrowseFolderAlertingPage')
+ : () => import(/* webpackChunkName: "FolderAlerting"*/ 'app/features/folders/FolderAlerting')
),
},
{