Nested folders: Add folder actions to other tabs (#68673)

* add folder actions to other tabs

* fix copy pasta

* add unit tests

* don't need tree here

* fixes some copy pasta

* move into separate fixtures file
This commit is contained in:
Ashley Harrison 2023-05-24 10:41:03 +01:00 committed by GitHub
parent 4980b64274
commit a8f91f115c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 620 additions and 59 deletions

View File

@ -133,39 +133,41 @@ export const Permissions = ({
return (
<div>
{config.featureToggles.nestedFolders && resource === 'folders' && (
{canSetPermissions && (
<>
This will change permissions for this folder and all its descendants. In total, this will affect:
<DescendantCount
selectedItems={{
folder: { [resourceId]: true },
dashboard: {},
panel: {},
$all: false,
}}
/>
<Space v={2} />
{config.featureToggles.nestedFolders && resource === 'folders' && (
<>
This will change permissions for this folder and all its descendants. In total, this will affect:
<DescendantCount
selectedItems={{
folder: { [resourceId]: true },
dashboard: {},
panel: {},
$all: false,
}}
/>
<Space v={2} />
</>
)}
<Button
className={styles.addPermissionButton}
variant={'primary'}
key="add-permission"
onClick={() => setIsAdding(true)}
>
{buttonLabel}
</Button>
<SlideDown in={isAdding}>
<AddPermission
title={addPermissionTitle}
onAdd={onAdd}
permissions={desc.permissions}
assignments={desc.assignments}
onCancel={() => setIsAdding(false)}
/>
</SlideDown>
</>
)}
{canSetPermissions && (
<Button
className={styles.addPermissionButton}
variant={'primary'}
key="add-permission"
onClick={() => setIsAdding(true)}
>
{buttonLabel}
</Button>
)}
<SlideDown in={isAdding}>
<AddPermission
title={addPermissionTitle}
onAdd={onAdd}
permissions={desc.permissions}
assignments={desc.assignments}
onCancel={() => setIsAdding(false)}
/>
</SlideDown>
{items.length === 0 && (
<table className="filter-table gf-form-group">
<tbody>

View File

@ -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
);
}

View File

@ -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<typeof rtlRender>) {
rtlRender(<TestProvider>{ui}</TestProvider>, 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(<BrowseFolderAlertingPage {...props} />);
expect(await screen.findByRole('heading', { name: mockFolderName })).toBeInTheDocument();
});
it('displays the "Folder actions" button', async () => {
render(<BrowseFolderAlertingPage {...props} />);
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(<BrowseFolderAlertingPage {...props} />);
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(<BrowseFolderAlertingPage {...props} />);
const ruleName = mockPrometheusRulesResponse.data.groups[0].rules[0].name;
expect(await screen.findByRole('link', { name: ruleName })).toBeInTheDocument();
});
});

View File

@ -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 (
<Page
navId="dashboards/browse"
pageNav={navModel}
actions={<>{folderDTO && <FolderActionsButton folder={folderDTO} />}</>}
>
<Page.Contents isLoading={isLoading}>
<AlertsFolderView folder={folder} />
</Page.Contents>
</Page>
);
}
export default BrowseFolderAlertingPage;

View File

@ -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<typeof rtlRender>) {
rtlRender(<TestProvider>{ui}</TestProvider>, 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(<BrowseFolderLibraryPanelsPage {...props} />);
expect(await screen.findByRole('heading', { name: mockFolderName })).toBeInTheDocument();
});
it('displays the "Folder actions" button', async () => {
render(<BrowseFolderLibraryPanelsPage {...props} />);
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(<BrowseFolderLibraryPanelsPage {...props} />);
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(<BrowseFolderLibraryPanelsPage {...props} />);
expect(await screen.findByText(mockLibraryElementsResponse.elements[0].name)).toBeInTheDocument();
});
});

View File

@ -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<LibraryElementDTO | undefined>(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 (
<Page
navId="dashboards/browse"
pageNav={navModel}
actions={<>{folderDTO && <FolderActionsButton folder={folderDTO} />}</>}
>
<Page.Contents isLoading={isLoading}>
<LibraryPanelsSearch
onClick={setSelected}
currentFolderUID={folderUID}
showSecondaryActions
showSort
showPanelFilter
/>
{selected ? <OpenLibraryPanelModal onDismiss={() => setSelected(undefined)} libraryPanel={selected} /> : null}
</Page.Contents>
</Page>
);
}
export default BrowseFolderLibraryPanelsPage;

View File

@ -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<FolderDTO, string>({
query: (folderUID) => ({ url: `/folders/${folderUID}`, params: { accesscontrol: true } }),
providesTags: (_result, _error, arg) => [{ type: 'getFolder', id: arg }],
}),
getAffectedItems: builder.query<DescendantCount, DashboardTreeSelection>({
queryFn: async (selectedItems) => {
@ -67,8 +69,16 @@ export const browseDashboardsAPI = createApi({
return { data: totalCounts };
},
}),
moveFolder: builder.mutation<void, { folderUID: string; destinationUID: string }>({
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';

View File

@ -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);

View File

@ -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 = (
<Menu>
{canCreateDashboard && (
@ -33,10 +34,10 @@ export function CreateNewButton({ inFolder, canCreateDashboard, canCreateFolder
);
return (
<Dropdown overlay={newMenu}>
<Dropdown overlay={newMenu} onVisibleChange={setIsOpen}>
<Button>
{getNewPhrase()}
<Icon name="angle-down" />
<Icon name={isOpen ? 'angle-up' : 'angle-down'} />
</Button>
</Dropdown>
);

View File

@ -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 = (
<Menu>
<MenuItem onClick={() => setShowPermissionsDrawer(true)} label="Set permissions" />
{canViewPermissions && <MenuItem onClick={() => setShowPermissionsDrawer(true)} label="Manage permissions" />}
{canMoveFolder && <MenuItem onClick={showMoveModal} label="Move" />}
{canDeleteFolder && <MenuItem destructive onClick={showDeleteModal} label="Delete" />}
</Menu>
);
if (!canViewPermissions && !canMoveFolder && !canDeleteFolder) {
return null;
}
return (
<>
<Dropdown overlay={menu}>
<Dropdown overlay={menu} onVisibleChange={setIsOpen}>
<Button variant="secondary">
Folder actions
<Icon name="angle-down" />
<Icon name={isOpen ? 'angle-up' : 'angle-down'} />
</Button>
</Dropdown>
{showPermissionsDrawer && (

View File

@ -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,
},
},
};
}

View File

@ -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<LibraryPanel>): 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,
};
}

View File

@ -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 });
}
);

View File

@ -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<FolderDTO>): FolderState => {
return {
...state,
...action.payload,
hasChanged: false,
};
};
const folderSlice = createSlice({
name: 'folder',
initialState,
reducers: {
loadFolder: (state, action: PayloadAction<FolderDTO>): FolderState => {
return {
...state,
...action.payload,
hasChanged: false,
};
},
loadFolder: loadFolderReducer,
setFolderTitle: (state, action: PayloadAction<string>): 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;

View File

@ -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')
),
},
{