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(false)} + /> + )} - {canSetPermissions && ( - - )} - - 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 ( - + ); 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 ( <> - + {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') ), }, {