mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
4980b64274
commit
a8f91f115c
@ -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>
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
@ -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';
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 && (
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
@ -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 });
|
||||
}
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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')
|
||||
),
|
||||
},
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user