mirror of https://github.com/grafana/grafana
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 filepull/66906/head
parent
4980b64274
commit
a8f91f115c
@ -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; |
||||
@ -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, |
||||
}; |
||||
} |
||||
Loading…
Reference in new issue