3
0
mirror of https://github.com/grafana/grafana.git synced 2025-02-25 18:55:37 -06:00

Scopes: Group suggested dashboards ()

This commit is contained in:
Bogdan Matei 2024-08-26 16:08:27 +03:00 committed by GitHub
parent 1dd830b9f1
commit 605bc811d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 2131 additions and 1195 deletions

View File

@ -2,6 +2,7 @@ export interface ScopeDashboardBindingSpec {
dashboard: string;
dashboardTitle: string;
scope: string;
groups?: string[];
}
// TODO: Use Resource from apiserver when we export the types

View File

@ -1,3 +1,5 @@
import { isEqual } from 'lodash';
import { SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { scopesSelectorScene } from './instance';
@ -20,7 +22,7 @@ export class ScopesFacade extends SceneObjectBase<ScopesFacadeState> {
this._subs.add(
scopesSelectorScene?.subscribeToState((newState, prevState) => {
if (!newState.isLoadingScopes && (prevState.isLoadingScopes || newState.scopes !== prevState.scopes)) {
if (!newState.isLoadingScopes && (prevState.isLoadingScopes || !isEqual(newState.scopes, prevState.scopes))) {
this.state.handler?.(this);
}
})

View File

@ -1,23 +1,27 @@
import { css, cx } from '@emotion/css';
import { isEqual } from 'lodash';
import { Link } from 'react-router-dom';
import { GrafanaTheme2, urlUtil } from '@grafana/data';
import { GrafanaTheme2, ScopeDashboardBinding } from '@grafana/data';
import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState } from '@grafana/scenes';
import { Button, CustomScrollbar, FilterInput, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { Button, CustomScrollbar, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { ScopesDashboardsTree } from './ScopesDashboardsTree';
import { ScopesDashboardsTreeSearch } from './ScopesDashboardsTreeSearch';
import { ScopesSelectorScene } from './ScopesSelectorScene';
import { fetchSuggestedDashboards } from './api';
import { fetchDashboards } from './api';
import { DASHBOARDS_OPENED_KEY } from './const';
import { SuggestedDashboard } from './types';
import { getScopeNamesFromSelectedScopes } from './utils';
import { SuggestedDashboardsFoldersMap } from './types';
import { filterFolders, getScopeNamesFromSelectedScopes, groupDashboards } from './utils';
export interface ScopesDashboardsSceneState extends SceneObjectState {
selector: SceneObjectRef<ScopesSelectorScene> | null;
dashboards: SuggestedDashboard[];
filteredDashboards: SuggestedDashboard[];
// by keeping a track of the raw response, it's much easier to check if we got any dashboards for the currently selected scopes
dashboards: ScopeDashboardBinding[];
// this is a grouping in folders of the `dashboards` property. it is used for filtering the dashboards and folders when the search query changes
folders: SuggestedDashboardsFoldersMap;
// a filtered version of the `folders` property. this prevents a lot of unnecessary parsings in React renders
filteredFolders: SuggestedDashboardsFoldersMap;
forScopeNames: string[];
isLoading: boolean;
isPanelOpened: boolean;
@ -28,7 +32,8 @@ export interface ScopesDashboardsSceneState extends SceneObjectState {
export const getInitialDashboardsState: () => Omit<ScopesDashboardsSceneState, 'selector'> = () => ({
dashboards: [],
filteredDashboards: [],
folders: {},
filteredFolders: {},
forScopeNames: [],
isLoading: false,
isPanelOpened: localStorage.getItem(DASHBOARDS_OPENED_KEY) === 'true',
@ -80,7 +85,8 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
if (scopeNames.length === 0) {
return this.setState({
dashboards: [],
filteredDashboards: [],
folders: {},
filteredFolders: {},
forScopeNames: [],
isLoading: false,
scopesSelected: false,
@ -89,11 +95,14 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
this.setState({ isLoading: true });
const dashboards = await fetchSuggestedDashboards(scopeNames);
const dashboards = await fetchDashboards(scopeNames);
const folders = groupDashboards(dashboards);
const filteredFolders = filterFolders(folders, this.state.searchQuery);
this.setState({
dashboards,
filteredDashboards: this.filterDashboards(dashboards, this.state.searchQuery),
folders,
filteredFolders,
forScopeNames: scopeNames,
isLoading: false,
scopesSelected: scopeNames.length > 0,
@ -101,14 +110,35 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
}
public changeSearchQuery(searchQuery: string) {
searchQuery = searchQuery ?? '';
this.setState({
filteredDashboards: searchQuery
? this.filterDashboards(this.state.dashboards, searchQuery)
: this.state.dashboards,
searchQuery: searchQuery ?? '',
filteredFolders: filterFolders(this.state.folders, searchQuery),
searchQuery,
});
}
public updateFolder(path: string[], isExpanded: boolean) {
let folders = { ...this.state.folders };
let filteredFolders = { ...this.state.filteredFolders };
let currentLevelFolders: SuggestedDashboardsFoldersMap = folders;
let currentLevelFilteredFolders: SuggestedDashboardsFoldersMap = filteredFolders;
for (let idx = 0; idx < path.length - 1; idx++) {
currentLevelFolders = currentLevelFolders[path[idx]].folders;
currentLevelFilteredFolders = currentLevelFilteredFolders[path[idx]].folders;
}
const name = path[path.length - 1];
const currentFolder = currentLevelFolders[name];
const currentFilteredFolder = currentLevelFilteredFolders[name];
currentFolder.isExpanded = isExpanded;
currentFilteredFolder.isExpanded = isExpanded;
this.setState({ folders, filteredFolders });
}
public togglePanel() {
if (this.state.isPanelOpened) {
this.closePanel();
@ -135,20 +165,13 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
public disable() {
this.setState({ isEnabled: false });
}
private filterDashboards(dashboards: SuggestedDashboard[], searchQuery: string): SuggestedDashboard[] {
const lowerCasedSearchQuery = searchQuery.toLowerCase();
return dashboards.filter(({ dashboardTitle }) => dashboardTitle.toLowerCase().includes(lowerCasedSearchQuery));
}
}
export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps<ScopesDashboardsScene>) {
const { dashboards, filteredDashboards, isLoading, isPanelOpened, isEnabled, searchQuery, scopesSelected } =
const { dashboards, filteredFolders, isLoading, isPanelOpened, isEnabled, searchQuery, scopesSelected } =
model.useState();
const styles = useStyles2(getStyles);
const [queryParams] = useQueryParams();
const styles = useStyles2(getStyles);
if (!isEnabled || !isPanelOpened) {
return null;
@ -178,15 +201,11 @@ export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps<Sco
return (
<div className={styles.container} data-testid="scopes-dashboards-container">
<div className={styles.searchInputContainer}>
<FilterInput
disabled={isLoading}
placeholder={t('scopes.dashboards.search', 'Search')}
value={searchQuery}
data-testid="scopes-dashboards-search"
onChange={(value) => model.changeSearchQuery(value)}
/>
</div>
<ScopesDashboardsTreeSearch
disabled={isLoading}
query={searchQuery}
onChange={(value) => model.changeSearchQuery(value)}
/>
{isLoading ? (
<LoadingPlaceholder
@ -194,18 +213,13 @@ export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps<Sco
text={t('scopes.dashboards.loading', 'Loading dashboards')}
data-testid="scopes-dashboards-loading"
/>
) : filteredDashboards.length > 0 ? (
) : filteredFolders[''] ? (
<CustomScrollbar>
{filteredDashboards.map(({ dashboard, dashboardTitle }) => (
<Link
key={dashboard}
to={urlUtil.renderUrl(`/d/${dashboard}/`, queryParams)}
className={styles.dashboardItem}
data-testid={`scopes-dashboards-${dashboard}`}
>
{dashboardTitle}
</Link>
))}
<ScopesDashboardsTree
folders={filteredFolders}
folderPath={['']}
onFolderUpdate={(path, isExpanded) => model.updateFolder(path, isExpanded)}
/>
</CustomScrollbar>
) : (
<p className={styles.noResultsContainer} data-testid="scopes-dashboards-notFoundForFilter">
@ -246,19 +260,8 @@ const getStyles = (theme: GrafanaTheme2) => {
margin: 0,
textAlign: 'center',
}),
searchInputContainer: css({
flex: '0 1 auto',
}),
loadingIndicator: css({
alignSelf: 'center',
}),
dashboardItem: css({
padding: theme.spacing(1, 0),
borderBottom: `1px solid ${theme.colors.border.weak}`,
'& :is(:first-child)': {
paddingTop: 0,
},
}),
};
};

View File

@ -0,0 +1,32 @@
import { ScopesDashboardsTreeDashboardItem } from './ScopesDashboardsTreeDashboardItem';
import { ScopesDashboardsTreeFolderItem } from './ScopesDashboardsTreeFolderItem';
import { OnFolderUpdate, SuggestedDashboardsFoldersMap } from './types';
export interface ScopesDashboardsTreeProps {
folders: SuggestedDashboardsFoldersMap;
folderPath: string[];
onFolderUpdate: OnFolderUpdate;
}
export function ScopesDashboardsTree({ folders, folderPath, onFolderUpdate }: ScopesDashboardsTreeProps) {
const folderId = folderPath[folderPath.length - 1];
const folder = folders[folderId];
return (
<div role="tree">
{Object.entries(folder.folders).map(([subFolderId, subFolder]) => (
<ScopesDashboardsTreeFolderItem
key={subFolderId}
folder={subFolder}
folders={folder.folders}
folderPath={[...folderPath, subFolderId]}
onFolderUpdate={onFolderUpdate}
/>
))}
{Object.values(folder.dashboards).map((dashboard) => (
<ScopesDashboardsTreeDashboardItem key={dashboard.dashboard} dashboard={dashboard} />
))}
</div>
);
}

View File

@ -0,0 +1,45 @@
import { css } from '@emotion/css';
import { Link } from 'react-router-dom';
import { GrafanaTheme2, urlUtil } from '@grafana/data';
import { Icon, useStyles2 } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { SuggestedDashboard } from './types';
export interface ScopesDashboardsTreeDashboardItemProps {
dashboard: SuggestedDashboard;
}
export function ScopesDashboardsTreeDashboardItem({ dashboard }: ScopesDashboardsTreeDashboardItemProps) {
const styles = useStyles2(getStyles);
const [queryParams] = useQueryParams();
return (
<Link
key={dashboard.dashboard}
to={urlUtil.renderUrl(`/d/${dashboard.dashboard}/`, queryParams)}
className={styles.container}
data-testid={`scopes-dashboards-${dashboard.dashboard}`}
role="treeitem"
>
<Icon name="apps" /> {dashboard.dashboardTitle}
</Link>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
container: css({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
padding: theme.spacing(0.5, 0),
'&:last-child': css({
paddingBottom: 0,
}),
}),
};
};

View File

@ -0,0 +1,71 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Icon, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { ScopesDashboardsTree } from './ScopesDashboardsTree';
import { OnFolderUpdate, SuggestedDashboardsFolder, SuggestedDashboardsFoldersMap } from './types';
export interface ScopesDashboardsTreeFolderItemProps {
folder: SuggestedDashboardsFolder;
folderPath: string[];
folders: SuggestedDashboardsFoldersMap;
onFolderUpdate: OnFolderUpdate;
}
export function ScopesDashboardsTreeFolderItem({
folder,
folderPath,
folders,
onFolderUpdate,
}: ScopesDashboardsTreeFolderItemProps) {
const styles = useStyles2(getStyles);
return (
<div className={styles.container} role="treeitem" aria-selected={folder.isExpanded}>
<button
className={styles.expand}
data-testid={`scopes-dashboards-${folder.title}-expand`}
aria-label={
folder.isExpanded ? t('scopes.dashboards.collapse', 'Collapse') : t('scopes.dashboards.expand', 'Expand')
}
onClick={() => {
onFolderUpdate(folderPath, !folder.isExpanded);
}}
>
<Icon name={!folder.isExpanded ? 'angle-right' : 'angle-down'} />
{folder.title}
</button>
{folder.isExpanded && (
<div className={styles.children}>
<ScopesDashboardsTree folders={folders} folderPath={folderPath} onFolderUpdate={onFolderUpdate} />
</div>
)}
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
container: css({
display: 'flex',
flexDirection: 'column',
padding: theme.spacing(0.5, 0),
}),
expand: css({
alignItems: 'center',
background: 'none',
border: 0,
display: 'flex',
gap: theme.spacing(1),
margin: 0,
padding: 0,
}),
children: css({
paddingLeft: theme.spacing(4),
}),
};
};

View File

@ -0,0 +1,55 @@
import { css } from '@emotion/css';
import { useEffect, useState } from 'react';
import { useDebounce } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { FilterInput, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
export interface ScopesDashboardsTreeSearchProps {
disabled: boolean;
query: string;
onChange: (value: string) => void;
}
export function ScopesDashboardsTreeSearch({ disabled, query, onChange }: ScopesDashboardsTreeSearchProps) {
const styles = useStyles2(getStyles);
const [inputState, setInputState] = useState<{ value: string; isDirty: boolean }>({ value: query, isDirty: false });
const [getDebounceState] = useDebounce(
() => {
if (inputState.isDirty) {
onChange(inputState.value);
}
},
500,
[inputState.isDirty, inputState.value]
);
useEffect(() => {
if ((getDebounceState() || !inputState.isDirty) && inputState.value !== query) {
setInputState({ value: query, isDirty: false });
}
}, [getDebounceState, inputState, query]);
return (
<div className={styles.container}>
<FilterInput
disabled={disabled}
placeholder={t('scopes.dashboards.search', 'Search')}
value={inputState.value}
data-testid="scopes-dashboards-search"
onChange={(value) => setInputState({ value, isDirty: true })}
/>
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
container: css({
flex: '0 1 auto',
}),
};
};

View File

@ -2,7 +2,7 @@ import { Scope, ScopeDashboardBinding, ScopeNode, ScopeSpec } from '@grafana/dat
import { config, getBackendSrv } from '@grafana/runtime';
import { ScopedResourceClient } from 'app/features/apiserver/client';
import { NodeReason, NodesMap, SelectedScope, SuggestedDashboard, TreeScope } from './types';
import { NodeReason, NodesMap, SelectedScope, TreeScope } from './types';
import { getBasicScope, mergeScopes } from './utils';
const group = 'scope.grafana.app';
@ -98,23 +98,3 @@ export async function fetchDashboards(scopeNames: string[]): Promise<ScopeDashbo
return [];
}
}
export async function fetchSuggestedDashboards(scopeNames: string[]): Promise<SuggestedDashboard[]> {
const items = await fetchDashboards(scopeNames);
return Object.values(
items.reduce<Record<string, SuggestedDashboard>>((acc, item) => {
if (!acc[item.spec.dashboard]) {
acc[item.spec.dashboard] = {
dashboard: item.spec.dashboard,
dashboardTitle: item.spec.dashboardTitle,
items: [],
};
}
acc[item.spec.dashboard].items.push(item);
return acc;
}, {})
);
}

View File

@ -33,5 +33,16 @@ export interface SuggestedDashboard {
items: ScopeDashboardBinding[];
}
export interface SuggestedDashboardsFolder {
title: string;
isExpanded: boolean;
folders: SuggestedDashboardsFoldersMap;
dashboards: SuggestedDashboardsMap;
}
export type SuggestedDashboardsMap = Record<string, SuggestedDashboard>;
export type SuggestedDashboardsFoldersMap = Record<string, SuggestedDashboardsFolder>;
export type OnNodeUpdate = (path: string[], isExpanded: boolean, query: string) => void;
export type OnNodeSelectToggle = (path: string[]) => void;
export type OnFolderUpdate = (path: string[], isExpanded: boolean) => void;

View File

@ -1,6 +1,6 @@
import { Scope } from '@grafana/data';
import { Scope, ScopeDashboardBinding } from '@grafana/data';
import { SelectedScope, TreeScope } from './types';
import { SelectedScope, SuggestedDashboardsFoldersMap, TreeScope } from './types';
export function getBasicScope(name: string): Scope {
return {
@ -43,3 +43,82 @@ export function getScopesFromSelectedScopes(scopes: SelectedScope[]): Scope[] {
export function getScopeNamesFromSelectedScopes(scopes: SelectedScope[]): string[] {
return scopes.map(({ scope }) => scope.metadata.name);
}
export function groupDashboards(dashboards: ScopeDashboardBinding[]): SuggestedDashboardsFoldersMap {
return dashboards.reduce<SuggestedDashboardsFoldersMap>(
(acc, dashboard) => {
const rootNode = acc[''];
const groups = dashboard.spec.groups ?? [];
groups.forEach((group) => {
if (group && !rootNode.folders[group]) {
rootNode.folders[group] = {
title: group,
isExpanded: false,
folders: {},
dashboards: {},
};
}
});
const targets =
groups.length > 0
? groups.map((group) => (group === '' ? rootNode.dashboards : rootNode.folders[group].dashboards))
: [rootNode.dashboards];
targets.forEach((target) => {
if (!target[dashboard.spec.dashboard]) {
target[dashboard.spec.dashboard] = {
dashboard: dashboard.spec.dashboard,
dashboardTitle: dashboard.spec.dashboardTitle,
items: [],
};
}
target[dashboard.spec.dashboard].items.push(dashboard);
});
return acc;
},
{
'': {
title: '',
isExpanded: true,
folders: {},
dashboards: {},
},
}
);
}
export function filterFolders(folders: SuggestedDashboardsFoldersMap, query: string): SuggestedDashboardsFoldersMap {
query = (query ?? '').toLowerCase();
return Object.entries(folders).reduce<SuggestedDashboardsFoldersMap>((acc, [folderId, folder]) => {
// If folder matches the query, we show everything inside
if (folder.title.toLowerCase().includes(query)) {
acc[folderId] = {
...folder,
isExpanded: true,
};
return acc;
}
const filteredFolders = filterFolders(folder.folders, query);
const filteredDashboards = Object.entries(folder.dashboards).filter(([_, dashboard]) =>
dashboard.dashboardTitle.toLowerCase().includes(query)
);
if (Object.keys(filteredFolders).length > 0 || filteredDashboards.length > 0) {
acc[folderId] = {
...folder,
isExpanded: true,
folders: filteredFolders,
dashboards: Object.fromEntries(filteredDashboards),
};
}
return acc;
}, {});
}

View File

@ -1,634 +0,0 @@
import { act, cleanup, waitFor } from '@testing-library/react';
import userEvents from '@testing-library/user-event';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { config, locationService, setPluginImportUtils } from '@grafana/runtime';
import { sceneGraph } from '@grafana/scenes';
import { getDashboardAPI, setDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
import { initializeScopes, scopesDashboardsScene, scopesSelectorScene } from './instance';
import {
buildTestScene,
fetchNodesSpy,
fetchScopeSpy,
fetchSelectedScopesSpy,
fetchSuggestedDashboardsSpy,
getDashboard,
getDashboardsExpand,
getDashboardsSearch,
getMock,
getNotFoundForFilter,
getNotFoundForFilterClear,
getNotFoundForScope,
getNotFoundNoScopes,
getPersistedApplicationsSlothPictureFactorySelect,
getPersistedApplicationsSlothPictureFactoryTitle,
getPersistedApplicationsSlothVoteTrackerTitle,
getResultApplicationsClustersExpand,
getResultApplicationsClustersSelect,
getResultApplicationsClustersSlothClusterNorthSelect,
getResultApplicationsClustersSlothClusterSouthSelect,
getResultApplicationsExpand,
getResultApplicationsSlothPictureFactorySelect,
getResultApplicationsSlothPictureFactoryTitle,
getResultApplicationsSlothVoteTrackerSelect,
getResultApplicationsSlothVoteTrackerTitle,
getResultClustersExpand,
getResultClustersSelect,
getResultClustersSlothClusterEastRadio,
getResultClustersSlothClusterNorthRadio,
getResultClustersSlothClusterSouthRadio,
getSelectorApply,
getSelectorCancel,
getSelectorInput,
getTreeHeadline,
getTreeSearch,
mocksScopes,
queryAllDashboard,
queryDashboard,
queryDashboardsContainer,
queryDashboardsSearch,
queryPersistedApplicationsSlothPictureFactoryTitle,
queryPersistedApplicationsSlothVoteTrackerTitle,
queryResultApplicationsClustersTitle,
queryResultApplicationsSlothPictureFactoryTitle,
queryResultApplicationsSlothVoteTrackerTitle,
querySelectorApply,
renderDashboard,
resetScenes,
} from './testUtils';
import { getClosestScopesFacade } from './utils';
jest.mock('@grafana/runtime', () => ({
__esModule: true,
...jest.requireActual('@grafana/runtime'),
useChromeHeaderHeight: jest.fn(),
getBackendSrv: () => ({
get: getMock,
}),
usePluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }),
}));
const panelPlugin = getPanelPlugin({
id: 'table',
skipDataQuery: true,
});
config.panels['table'] = panelPlugin.meta;
setPluginImportUtils({
importPanelPlugin: (id: string) => Promise.resolve(panelPlugin),
getPanelPluginFromCache: (id: string) => undefined,
});
describe('Scopes', () => {
describe('Feature flag off', () => {
beforeAll(() => {
config.featureToggles.scopeFilters = false;
config.featureToggles.groupByVariable = true;
initializeScopes();
});
it('Does not initialize', () => {
const dashboardScene = buildTestScene();
dashboardScene.activate();
expect(scopesSelectorScene).toBeNull();
});
});
describe('Feature flag on', () => {
let dashboardScene: DashboardScene;
beforeAll(() => {
config.featureToggles.scopeFilters = true;
config.featureToggles.groupByVariable = true;
});
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation(jest.fn());
fetchNodesSpy.mockClear();
fetchScopeSpy.mockClear();
fetchSelectedScopesSpy.mockClear();
fetchSuggestedDashboardsSpy.mockClear();
getMock.mockClear();
initializeScopes();
dashboardScene = buildTestScene();
renderDashboard(dashboardScene);
});
afterEach(() => {
resetScenes();
cleanup();
});
describe('Tree', () => {
it('Navigates through scopes nodes', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsClustersExpand());
await userEvents.click(getResultApplicationsExpand());
});
it('Fetches scope details on select', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
await waitFor(() => expect(fetchScopeSpy).toHaveBeenCalledTimes(1));
});
it('Selects the proper scopes', async () => {
await act(async () =>
scopesSelectorScene?.updateScopes([
{ scopeName: 'slothPictureFactory', path: [] },
{ scopeName: 'slothVoteTracker', path: [] },
])
);
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
expect(getResultApplicationsSlothVoteTrackerSelect()).toBeChecked();
expect(getResultApplicationsSlothPictureFactorySelect()).toBeChecked();
});
it('Can select scopes from same level', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getResultApplicationsClustersSelect());
await userEvents.click(getSelectorApply());
expect(getSelectorInput().value).toBe('slothVoteTracker, slothPictureFactory, Cluster Index Helper');
});
it('Can select a node from an inner level', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
await userEvents.click(getResultApplicationsClustersExpand());
await userEvents.click(getResultApplicationsClustersSlothClusterNorthSelect());
await userEvents.click(getSelectorApply());
expect(getSelectorInput().value).toBe('slothClusterNorth');
});
it('Can select a node from an upper level', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultClustersSelect());
await userEvents.click(getSelectorApply());
expect(getSelectorInput().value).toBe('Cluster Index Helper');
});
it('Respects only one select per container', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultClustersExpand());
await userEvents.click(getResultClustersSlothClusterNorthRadio());
expect(getResultClustersSlothClusterNorthRadio().checked).toBe(true);
expect(getResultClustersSlothClusterSouthRadio().checked).toBe(false);
await userEvents.click(getResultClustersSlothClusterSouthRadio());
expect(getResultClustersSlothClusterNorthRadio().checked).toBe(false);
expect(getResultClustersSlothClusterSouthRadio().checked).toBe(true);
});
it('Search works', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.type(getTreeSearch(), 'Clusters');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
expect(queryResultApplicationsSlothPictureFactoryTitle()).not.toBeInTheDocument();
expect(queryResultApplicationsSlothVoteTrackerTitle()).not.toBeInTheDocument();
expect(getResultApplicationsClustersSelect()).toBeInTheDocument();
await userEvents.clear(getTreeSearch());
await userEvents.type(getTreeSearch(), 'sloth');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(4));
expect(getResultApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
expect(getResultApplicationsSlothVoteTrackerSelect()).toBeInTheDocument();
expect(queryResultApplicationsClustersTitle()).not.toBeInTheDocument();
});
it('Opens to a selected scope', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultClustersExpand());
await userEvents.click(getSelectorApply());
await userEvents.click(getSelectorInput());
expect(queryResultApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
});
it('Persists a scope', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.type(getTreeSearch(), 'slothVoteTracker');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
expect(getPersistedApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
expect(queryPersistedApplicationsSlothVoteTrackerTitle()).not.toBeInTheDocument();
expect(queryResultApplicationsSlothPictureFactoryTitle()).not.toBeInTheDocument();
expect(getResultApplicationsSlothVoteTrackerTitle()).toBeInTheDocument();
});
it('Does not persist a retrieved scope', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.type(getTreeSearch(), 'slothPictureFactory');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
expect(queryPersistedApplicationsSlothPictureFactoryTitle()).not.toBeInTheDocument();
expect(getResultApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
});
it('Removes persisted nodes', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.type(getTreeSearch(), 'slothVoteTracker');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
await userEvents.clear(getTreeSearch());
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(4));
expect(queryPersistedApplicationsSlothPictureFactoryTitle()).not.toBeInTheDocument();
expect(queryPersistedApplicationsSlothVoteTrackerTitle()).not.toBeInTheDocument();
expect(getResultApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
expect(getResultApplicationsSlothVoteTrackerTitle()).toBeInTheDocument();
});
it('Persists nodes from search', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.type(getTreeSearch(), 'sloth');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
await userEvents.type(getTreeSearch(), 'slothunknown');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(4));
expect(getPersistedApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
expect(getPersistedApplicationsSlothVoteTrackerTitle()).toBeInTheDocument();
await userEvents.clear(getTreeSearch());
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(5));
expect(getResultApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
expect(getResultApplicationsSlothVoteTrackerTitle()).toBeInTheDocument();
});
it('Selects a persisted scope', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.type(getTreeSearch(), 'slothVoteTracker');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
await userEvents.click(getSelectorApply());
expect(getSelectorInput().value).toBe('slothPictureFactory, slothVoteTracker');
});
it('Deselects a persisted scope', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.type(getTreeSearch(), 'slothVoteTracker');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
await userEvents.click(getSelectorApply());
expect(getSelectorInput().value).toBe('slothPictureFactory, slothVoteTracker');
await userEvents.click(getSelectorInput());
await userEvents.click(getPersistedApplicationsSlothPictureFactorySelect());
await userEvents.click(getSelectorApply());
expect(getSelectorInput().value).toBe('slothVoteTracker');
});
it('Shows the proper headline', async () => {
await userEvents.click(getSelectorInput());
expect(getTreeHeadline()).toHaveTextContent('Recommended');
await userEvents.type(getTreeSearch(), 'Applications');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(2));
expect(getTreeHeadline()).toHaveTextContent('Results');
await userEvents.type(getTreeSearch(), 'unknown');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
expect(getTreeHeadline()).toHaveTextContent('No results found for your query');
});
});
describe('Selector', () => {
it('Opens', async () => {
await userEvents.click(getSelectorInput());
expect(getSelectorApply()).toBeInTheDocument();
});
it('Fetches scope details on save', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultClustersSelect());
await userEvents.click(getSelectorApply());
await waitFor(() => expect(fetchSelectedScopesSpy).toHaveBeenCalled());
expect(getClosestScopesFacade(dashboardScene)?.value).toEqual(
mocksScopes.filter(({ metadata: { name } }) => name === 'indexHelperCluster')
);
});
it("Doesn't save the scopes on close", async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultClustersSelect());
await userEvents.click(getSelectorCancel());
await waitFor(() => expect(fetchSelectedScopesSpy).not.toHaveBeenCalled());
expect(getClosestScopesFacade(dashboardScene)?.value).toEqual([]);
});
it('Shows selected scopes', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultClustersSelect());
await userEvents.click(getSelectorApply());
expect(getSelectorInput().value).toEqual('Cluster Index Helper');
});
});
describe('Dashboards list', () => {
it('Toggles expanded state', async () => {
await userEvents.click(getDashboardsExpand());
expect(getNotFoundNoScopes()).toBeInTheDocument();
});
it('Does not fetch dashboards list when the list is not expanded', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getSelectorApply());
await waitFor(() => expect(fetchSuggestedDashboardsSpy).not.toHaveBeenCalled());
});
it('Fetches dashboards list when the list is expanded', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getSelectorApply());
await waitFor(() => expect(fetchSuggestedDashboardsSpy).toHaveBeenCalled());
});
it('Fetches dashboards list when the list is expanded after scope selection', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getSelectorApply());
await userEvents.click(getDashboardsExpand());
await waitFor(() => expect(fetchSuggestedDashboardsSpy).toHaveBeenCalled());
});
it('Shows dashboards for multiple scopes', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getSelectorApply());
expect(getDashboard('1')).toBeInTheDocument();
expect(getDashboard('2')).toBeInTheDocument();
expect(queryDashboard('3')).not.toBeInTheDocument();
expect(queryDashboard('4')).not.toBeInTheDocument();
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
await userEvents.click(getSelectorApply());
expect(getDashboard('1')).toBeInTheDocument();
expect(getDashboard('2')).toBeInTheDocument();
expect(getDashboard('3')).toBeInTheDocument();
expect(getDashboard('4')).toBeInTheDocument();
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getSelectorApply());
expect(queryDashboard('1')).not.toBeInTheDocument();
expect(queryDashboard('2')).not.toBeInTheDocument();
expect(getDashboard('3')).toBeInTheDocument();
expect(getDashboard('4')).toBeInTheDocument();
});
it('Filters the dashboards list', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getSelectorApply());
expect(getDashboard('1')).toBeInTheDocument();
expect(getDashboard('2')).toBeInTheDocument();
await userEvents.type(getDashboardsSearch(), '1');
expect(queryDashboard('2')).not.toBeInTheDocument();
});
it('Deduplicates the dashboards list', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsClustersExpand());
await userEvents.click(getResultApplicationsClustersSlothClusterNorthSelect());
await userEvents.click(getResultApplicationsClustersSlothClusterSouthSelect());
await userEvents.click(getSelectorApply());
expect(queryAllDashboard('5')).toHaveLength(1);
expect(queryAllDashboard('6')).toHaveLength(1);
expect(queryAllDashboard('7')).toHaveLength(1);
expect(queryAllDashboard('8')).toHaveLength(1);
});
it('Does show a proper message when no scopes are selected', async () => {
await userEvents.click(getDashboardsExpand());
expect(getNotFoundNoScopes()).toBeInTheDocument();
expect(queryDashboardsSearch()).not.toBeInTheDocument();
});
it('Does not show the input when there are no dashboards found for scope', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getSelectorInput());
await userEvents.click(getResultClustersExpand());
await userEvents.click(getResultClustersSlothClusterEastRadio());
await userEvents.click(getSelectorApply());
expect(getNotFoundForScope()).toBeInTheDocument();
expect(queryDashboardsSearch()).not.toBeInTheDocument();
});
it('Does show the input and a message when there are no dashboards found for filter', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getSelectorApply());
await userEvents.type(getDashboardsSearch(), 'unknown');
expect(queryDashboardsSearch()).toBeInTheDocument();
expect(getNotFoundForFilter()).toBeInTheDocument();
await userEvents.click(getNotFoundForFilterClear());
expect(getDashboardsSearch().value).toBe('');
});
});
describe('View mode', () => {
it('Enters view mode', async () => {
await act(async () => dashboardScene.onEnterEditMode());
expect(scopesSelectorScene?.state?.isReadOnly).toEqual(true);
expect(scopesDashboardsScene?.state?.isPanelOpened).toEqual(false);
});
it('Closes selector on enter', async () => {
await userEvents.click(getSelectorInput());
await act(async () => dashboardScene.onEnterEditMode());
expect(querySelectorApply()).not.toBeInTheDocument();
});
it('Closes dashboards list on enter', async () => {
await userEvents.click(getDashboardsExpand());
await act(async () => dashboardScene.onEnterEditMode());
expect(queryDashboardsContainer()).not.toBeInTheDocument();
});
it('Does not open selector when view mode is active', async () => {
await act(async () => dashboardScene.onEnterEditMode());
await userEvents.click(getSelectorInput());
expect(querySelectorApply()).not.toBeInTheDocument();
});
it('Disables the expand button when view mode is active', async () => {
await act(async () => dashboardScene.onEnterEditMode());
expect(getDashboardsExpand()).toBeDisabled();
});
});
describe('Enrichers', () => {
it('Data requests', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getSelectorApply());
await waitFor(() => {
const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!;
expect(dashboardScene.enrichDataRequest(queryRunner).scopes).toEqual(
mocksScopes.filter(({ metadata: { name } }) => name === 'slothPictureFactory')
);
});
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
await userEvents.click(getSelectorApply());
await waitFor(() => {
const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!;
expect(dashboardScene.enrichDataRequest(queryRunner).scopes).toEqual(
mocksScopes.filter(
({ metadata: { name } }) => name === 'slothPictureFactory' || name === 'slothVoteTracker'
)
);
});
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getSelectorApply());
await waitFor(() => {
const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!;
expect(dashboardScene.enrichDataRequest(queryRunner).scopes).toEqual(
mocksScopes.filter(({ metadata: { name } }) => name === 'slothVoteTracker')
);
});
});
it('Filters requests', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getSelectorApply());
await waitFor(() => {
expect(dashboardScene.enrichFiltersRequest().scopes).toEqual(
mocksScopes.filter(({ metadata: { name } }) => name === 'slothPictureFactory')
);
});
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
await userEvents.click(getSelectorApply());
await waitFor(() => {
expect(dashboardScene.enrichFiltersRequest().scopes).toEqual(
mocksScopes.filter(
({ metadata: { name } }) => name === 'slothPictureFactory' || name === 'slothVoteTracker'
)
);
});
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getSelectorApply());
await waitFor(() => {
expect(dashboardScene.enrichFiltersRequest().scopes).toEqual(
mocksScopes.filter(({ metadata: { name } }) => name === 'slothVoteTracker')
);
});
});
});
});
describe('Dashboards API', () => {
describe('Feature flag off', () => {
beforeAll(() => {
config.featureToggles.scopeFilters = true;
config.featureToggles.passScopeToDashboardApi = false;
});
beforeEach(() => {
setDashboardAPI(undefined);
locationService.push('/?scopes=scope1&scopes=scope2&scopes=scope3');
});
afterEach(() => {
resetScenes();
cleanup();
});
it('Legacy API should not pass the scopes', async () => {
config.featureToggles.kubernetesDashboards = false;
getDashboardAPI().getDashboardDTO('1');
await waitFor(() => expect(getMock).toHaveBeenCalledWith('/api/dashboards/uid/1', undefined));
});
it('K8s API should not pass the scopes', async () => {
config.featureToggles.kubernetesDashboards = true;
getDashboardAPI().getDashboardDTO('1');
await waitFor(() =>
expect(getMock).toHaveBeenCalledWith(
'/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/1/dto'
)
);
});
});
describe('Feature flag on', () => {
beforeAll(() => {
config.featureToggles.scopeFilters = true;
config.featureToggles.passScopeToDashboardApi = true;
});
beforeEach(() => {
setDashboardAPI(undefined);
locationService.push('/?scopes=scope1&scopes=scope2&scopes=scope3');
initializeScopes();
});
afterEach(() => {
resetScenes();
cleanup();
});
it('Legacy API should pass the scopes', async () => {
config.featureToggles.kubernetesDashboards = false;
getDashboardAPI().getDashboardDTO('1');
await waitFor(() =>
expect(getMock).toHaveBeenCalledWith('/api/dashboards/uid/1', { scopes: ['scope1', 'scope2', 'scope3'] })
);
});
it('K8s API should not pass the scopes', async () => {
config.featureToggles.kubernetesDashboards = true;
getDashboardAPI().getDashboardDTO('1');
await waitFor(() =>
expect(getMock).toHaveBeenCalledWith(
'/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/1/dto'
)
);
});
});
});
});

View File

@ -1,480 +0,0 @@
import { screen } from '@testing-library/react';
import { KBarProvider } from 'kbar';
import { render } from 'test/test-utils';
import { Scope, ScopeDashboardBinding, ScopeNode } from '@grafana/data';
import {
AdHocFiltersVariable,
behaviors,
GroupByVariable,
sceneGraph,
SceneGridItem,
SceneGridLayout,
SceneQueryRunner,
SceneTimeRange,
SceneVariableSet,
VizPanel,
} from '@grafana/scenes';
import { AppChrome } from 'app/core/components/AppChrome/AppChrome';
import { DashboardControls } from 'app/features/dashboard-scene/scene//DashboardControls';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
import { ScopesFacade } from './ScopesFacadeScene';
import { scopesDashboardsScene, scopesSelectorScene } from './instance';
import { getInitialDashboardsState } from './internal/ScopesDashboardsScene';
import { initialSelectorState } from './internal/ScopesSelectorScene';
import * as api from './internal/api';
import { DASHBOARDS_OPENED_KEY } from './internal/const';
export const mocksScopes: Scope[] = [
{
metadata: { name: 'indexHelperCluster' },
spec: {
title: 'Cluster Index Helper',
type: 'indexHelper',
description: 'redundant label filter but makes queries faster',
category: 'indexHelpers',
filters: [{ key: 'indexHelper', value: 'cluster', operator: 'equals' }],
},
},
{
metadata: { name: 'slothClusterNorth' },
spec: {
title: 'slothClusterNorth',
type: 'cluster',
description: 'slothClusterNorth',
category: 'clusters',
filters: [{ key: 'cluster', value: 'slothClusterNorth', operator: 'equals' }],
},
},
{
metadata: { name: 'slothClusterSouth' },
spec: {
title: 'slothClusterSouth',
type: 'cluster',
description: 'slothClusterSouth',
category: 'clusters',
filters: [{ key: 'cluster', value: 'slothClusterSouth', operator: 'equals' }],
},
},
{
metadata: { name: 'slothClusterEast' },
spec: {
title: 'slothClusterEast',
type: 'cluster',
description: 'slothClusterEast',
category: 'clusters',
filters: [{ key: 'cluster', value: 'slothClusterEast', operator: 'equals' }],
},
},
{
metadata: { name: 'slothPictureFactory' },
spec: {
title: 'slothPictureFactory',
type: 'app',
description: 'slothPictureFactory',
category: 'apps',
filters: [{ key: 'app', value: 'slothPictureFactory', operator: 'equals' }],
},
},
{
metadata: { name: 'slothVoteTracker' },
spec: {
title: 'slothVoteTracker',
type: 'app',
description: 'slothVoteTracker',
category: 'apps',
filters: [{ key: 'app', value: 'slothVoteTracker', operator: 'equals' }],
},
},
] as const;
export const mocksScopeDashboardBindings: ScopeDashboardBinding[] = [
{
metadata: { name: 'binding1' },
spec: { dashboard: '1', dashboardTitle: 'My Dashboard 1', scope: 'slothPictureFactory' },
},
{
metadata: { name: 'binding2' },
spec: { dashboard: '2', dashboardTitle: 'My Dashboard 2', scope: 'slothPictureFactory' },
},
{
metadata: { name: 'binding3' },
spec: { dashboard: '3', dashboardTitle: 'My Dashboard 3', scope: 'slothVoteTracker' },
},
{
metadata: { name: 'binding4' },
spec: { dashboard: '4', dashboardTitle: 'My Dashboard 4', scope: 'slothVoteTracker' },
},
{
metadata: { name: 'binding5' },
spec: { dashboard: '5', dashboardTitle: 'My Dashboard 5', scope: 'slothClusterNorth' },
},
{
metadata: { name: 'binding6' },
spec: { dashboard: '6', dashboardTitle: 'My Dashboard 6', scope: 'slothClusterNorth' },
},
{
metadata: { name: 'binding7' },
spec: { dashboard: '7', dashboardTitle: 'My Dashboard 7', scope: 'slothClusterNorth' },
},
{
metadata: { name: 'binding8' },
spec: { dashboard: '5', dashboardTitle: 'My Dashboard 5', scope: 'slothClusterSouth' },
},
{
metadata: { name: 'binding9' },
spec: { dashboard: '6', dashboardTitle: 'My Dashboard 6', scope: 'slothClusterSouth' },
},
{
metadata: { name: 'binding10' },
spec: { dashboard: '8', dashboardTitle: 'My Dashboard 8', scope: 'slothClusterSouth' },
},
] as const;
export const mocksNodes: Array<ScopeNode & { parent: string }> = [
{
parent: '',
metadata: { name: 'applications' },
spec: {
nodeType: 'container',
title: 'Applications',
description: 'Application Scopes',
},
},
{
parent: '',
metadata: { name: 'clusters' },
spec: {
nodeType: 'container',
title: 'Clusters',
description: 'Cluster Scopes',
disableMultiSelect: true,
linkType: 'scope',
linkId: 'indexHelperCluster',
},
},
{
parent: 'applications',
metadata: { name: 'applications-slothPictureFactory' },
spec: {
nodeType: 'leaf',
title: 'slothPictureFactory',
description: 'slothPictureFactory',
linkType: 'scope',
linkId: 'slothPictureFactory',
},
},
{
parent: 'applications',
metadata: { name: 'applications-slothVoteTracker' },
spec: {
nodeType: 'leaf',
title: 'slothVoteTracker',
description: 'slothVoteTracker',
linkType: 'scope',
linkId: 'slothVoteTracker',
},
},
{
parent: 'applications',
metadata: { name: 'applications-clusters' },
spec: {
nodeType: 'container',
title: 'Clusters',
description: 'Application/Clusters Scopes',
linkType: 'scope',
linkId: 'indexHelperCluster',
},
},
{
parent: 'applications-clusters',
metadata: { name: 'applications-clusters-slothClusterNorth' },
spec: {
nodeType: 'leaf',
title: 'slothClusterNorth',
description: 'slothClusterNorth',
linkType: 'scope',
linkId: 'slothClusterNorth',
},
},
{
parent: 'applications-clusters',
metadata: { name: 'applications-clusters-slothClusterSouth' },
spec: {
nodeType: 'leaf',
title: 'slothClusterSouth',
description: 'slothClusterSouth',
linkType: 'scope',
linkId: 'slothClusterSouth',
},
},
{
parent: 'clusters',
metadata: { name: 'clusters-slothClusterNorth' },
spec: {
nodeType: 'leaf',
title: 'slothClusterNorth',
description: 'slothClusterNorth',
linkType: 'scope',
linkId: 'slothClusterNorth',
},
},
{
parent: 'clusters',
metadata: { name: 'clusters-slothClusterSouth' },
spec: {
nodeType: 'leaf',
title: 'slothClusterSouth',
description: 'slothClusterSouth',
linkType: 'scope',
linkId: 'slothClusterSouth',
},
},
{
parent: 'clusters',
metadata: { name: 'clusters-slothClusterEast' },
spec: {
nodeType: 'leaf',
title: 'slothClusterEast',
description: 'slothClusterEast',
linkType: 'scope',
linkId: 'slothClusterEast',
},
},
{
parent: 'clusters',
metadata: { name: 'clusters-applications' },
spec: {
nodeType: 'container',
title: 'Applications',
description: 'Clusters/Application Scopes',
},
},
{
parent: 'clusters-applications',
metadata: { name: 'clusters-applications-slothPictureFactory' },
spec: {
nodeType: 'leaf',
title: 'slothPictureFactory',
description: 'slothPictureFactory',
linkType: 'scope',
linkId: 'slothPictureFactory',
},
},
{
parent: 'clusters-applications',
metadata: { name: 'clusters-applications-slothVoteTracker' },
spec: {
nodeType: 'leaf',
title: 'slothVoteTracker',
description: 'slothVoteTracker',
linkType: 'scope',
linkId: 'slothVoteTracker',
},
},
] as const;
export const fetchNodesSpy = jest.spyOn(api, 'fetchNodes');
export const fetchScopeSpy = jest.spyOn(api, 'fetchScope');
export const fetchSelectedScopesSpy = jest.spyOn(api, 'fetchSelectedScopes');
export const fetchSuggestedDashboardsSpy = jest.spyOn(api, 'fetchSuggestedDashboards');
export const getMock = jest
.fn()
.mockImplementation((url: string, params: { parent: string; scope: string[]; query?: string }) => {
if (url.startsWith('/apis/scope.grafana.app/v0alpha1/namespaces/default/find/scope_node_children')) {
return {
items: mocksNodes.filter(
({ parent, spec: { title } }) => parent === params.parent && title.includes(params.query ?? '')
),
};
}
if (url.startsWith('/apis/scope.grafana.app/v0alpha1/namespaces/default/scopes/')) {
const name = url.replace('/apis/scope.grafana.app/v0alpha1/namespaces/default/scopes/', '');
return mocksScopes.find((scope) => scope.metadata.name === name) ?? {};
}
if (url.startsWith('/apis/scope.grafana.app/v0alpha1/namespaces/default/find/scope_dashboard_bindings')) {
return {
items: mocksScopeDashboardBindings.filter(({ spec: { scope: bindingScope } }) =>
params.scope.includes(bindingScope)
),
};
}
if (url.startsWith('/api/dashboards/uid/')) {
return {};
}
if (url.startsWith('/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/')) {
return {
metadata: {
name: '1',
},
};
}
return {};
});
const selectors = {
tree: {
search: 'scopes-tree-search',
headline: 'scopes-tree-headline',
select: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-checkbox`,
radio: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-radio`,
expand: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-expand`,
title: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-title`,
},
selector: {
input: 'scopes-selector-input',
container: 'scopes-selector-container',
loading: 'scopes-selector-loading',
apply: 'scopes-selector-apply',
cancel: 'scopes-selector-cancel',
},
dashboards: {
expand: 'scopes-dashboards-expand',
container: 'scopes-dashboards-container',
search: 'scopes-dashboards-search',
loading: 'scopes-dashboards-loading',
dashboard: (uid: string) => `scopes-dashboards-${uid}`,
notFoundNoScopes: 'scopes-dashboards-notFoundNoScopes',
notFoundForScope: 'scopes-dashboards-notFoundForScope',
notFoundForFilter: 'scopes-dashboards-notFoundForFilter',
notFoundForFilterClear: 'scopes-dashboards-notFoundForFilter-clear',
},
};
export const getSelectorInput = () => screen.getByTestId<HTMLInputElement>(selectors.selector.input);
export const querySelectorApply = () => screen.queryByTestId(selectors.selector.apply);
export const getSelectorApply = () => screen.getByTestId(selectors.selector.apply);
export const getSelectorCancel = () => screen.getByTestId(selectors.selector.cancel);
export const getDashboardsExpand = () => screen.getByTestId(selectors.dashboards.expand);
export const queryDashboardsContainer = () => screen.queryByTestId(selectors.dashboards.container);
export const queryDashboardsSearch = () => screen.queryByTestId(selectors.dashboards.search);
export const getDashboardsSearch = () => screen.getByTestId<HTMLInputElement>(selectors.dashboards.search);
export const queryAllDashboard = (uid: string) => screen.queryAllByTestId(selectors.dashboards.dashboard(uid));
export const queryDashboard = (uid: string) => screen.queryByTestId(selectors.dashboards.dashboard(uid));
export const getDashboard = (uid: string) => screen.getByTestId(selectors.dashboards.dashboard(uid));
export const getNotFoundNoScopes = () => screen.getByTestId(selectors.dashboards.notFoundNoScopes);
export const getNotFoundForScope = () => screen.getByTestId(selectors.dashboards.notFoundForScope);
export const getNotFoundForFilter = () => screen.getByTestId(selectors.dashboards.notFoundForFilter);
export const getNotFoundForFilterClear = () => screen.getByTestId(selectors.dashboards.notFoundForFilterClear);
export const getTreeSearch = () => screen.getByTestId<HTMLInputElement>(selectors.tree.search);
export const getTreeHeadline = () => screen.getByTestId(selectors.tree.headline);
export const getResultApplicationsExpand = () => screen.getByTestId(selectors.tree.expand('applications', 'result'));
export const queryResultApplicationsSlothPictureFactoryTitle = () =>
screen.queryByTestId(selectors.tree.title('applications-slothPictureFactory', 'result'));
export const getResultApplicationsSlothPictureFactoryTitle = () =>
screen.getByTestId(selectors.tree.title('applications-slothPictureFactory', 'result'));
export const getResultApplicationsSlothPictureFactorySelect = () =>
screen.getByTestId(selectors.tree.select('applications-slothPictureFactory', 'result'));
export const queryPersistedApplicationsSlothPictureFactoryTitle = () =>
screen.queryByTestId(selectors.tree.title('applications-slothPictureFactory', 'persisted'));
export const getPersistedApplicationsSlothPictureFactoryTitle = () =>
screen.getByTestId(selectors.tree.title('applications-slothPictureFactory', 'persisted'));
export const getPersistedApplicationsSlothPictureFactorySelect = () =>
screen.getByTestId(selectors.tree.select('applications-slothPictureFactory', 'persisted'));
export const queryResultApplicationsSlothVoteTrackerTitle = () =>
screen.queryByTestId(selectors.tree.title('applications-slothVoteTracker', 'result'));
export const getResultApplicationsSlothVoteTrackerTitle = () =>
screen.getByTestId(selectors.tree.title('applications-slothVoteTracker', 'result'));
export const getResultApplicationsSlothVoteTrackerSelect = () =>
screen.getByTestId(selectors.tree.select('applications-slothVoteTracker', 'result'));
export const queryPersistedApplicationsSlothVoteTrackerTitle = () =>
screen.queryByTestId(selectors.tree.title('applications-slothVoteTracker', 'persisted'));
export const getPersistedApplicationsSlothVoteTrackerTitle = () =>
screen.getByTestId(selectors.tree.title('applications-slothVoteTracker', 'persisted'));
export const queryResultApplicationsClustersTitle = () =>
screen.queryByTestId(selectors.tree.title('applications-clusters', 'result'));
export const getResultApplicationsClustersSelect = () =>
screen.getByTestId(selectors.tree.select('applications-clusters', 'result'));
export const getResultApplicationsClustersExpand = () =>
screen.getByTestId(selectors.tree.expand('applications-clusters', 'result'));
export const getResultApplicationsClustersSlothClusterNorthSelect = () =>
screen.getByTestId(selectors.tree.select('applications-clusters-slothClusterNorth', 'result'));
export const getResultApplicationsClustersSlothClusterSouthSelect = () =>
screen.getByTestId(selectors.tree.select('applications-clusters-slothClusterSouth', 'result'));
export const getResultClustersSelect = () => screen.getByTestId(selectors.tree.select('clusters', 'result'));
export const getResultClustersExpand = () => screen.getByTestId(selectors.tree.expand('clusters', 'result'));
export const getResultClustersSlothClusterNorthRadio = () =>
screen.getByTestId<HTMLInputElement>(selectors.tree.radio('clusters-slothClusterNorth', 'result'));
export const getResultClustersSlothClusterSouthRadio = () =>
screen.getByTestId<HTMLInputElement>(selectors.tree.radio('clusters-slothClusterSouth', 'result'));
export const getResultClustersSlothClusterEastRadio = () =>
screen.getByTestId<HTMLInputElement>(selectors.tree.radio('clusters-slothClusterEast', 'result'));
export function buildTestScene(overrides: Partial<DashboardScene> = {}) {
return new DashboardScene({
title: 'hello',
uid: 'dash-1',
description: 'hello description',
tags: ['tag1', 'tag2'],
editable: true,
$timeRange: new SceneTimeRange({
timeZone: 'browser',
}),
controls: new DashboardControls({}),
$behaviors: [
new behaviors.CursorSync({}),
new ScopesFacade({
handler: (facade) => sceneGraph.getTimeRange(facade).onRefresh(),
}),
],
$variables: new SceneVariableSet({
variables: [
new AdHocFiltersVariable({
name: 'adhoc',
datasource: { uid: 'my-ds-uid' },
}),
new GroupByVariable({
name: 'groupby',
datasource: { uid: 'my-ds-uid' },
}),
],
}),
body: new SceneGridLayout({
children: [
new SceneGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 300,
height: 300,
body: new VizPanel({
title: 'Panel A',
key: 'panel-1',
pluginId: 'table',
$data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
}),
}),
],
}),
...overrides,
});
}
export function renderDashboard(dashboardScene: DashboardScene) {
return render(
<KBarProvider>
<AppChrome>
<dashboardScene.Component model={dashboardScene} />
</AppChrome>
</KBarProvider>
);
}
export function resetScenes() {
scopesSelectorScene?.setState(initialSelectorState);
localStorage.removeItem(DASHBOARDS_OPENED_KEY);
scopesDashboardsScene?.setState(getInitialDashboardsState());
}

View File

@ -0,0 +1,84 @@
import { cleanup } from '@testing-library/react';
import { config, locationService } from '@grafana/runtime';
import { getDashboardAPI, setDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
import { initializeScopes } from '../instance';
import { getMock } from './utils/mocks';
import { resetScenes } from './utils/render';
jest.mock('@grafana/runtime', () => ({
__esModule: true,
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => ({
get: getMock,
}),
}));
describe('Scopes', () => {
describe('Dashboards API', () => {
describe('Feature flag off', () => {
beforeAll(() => {
config.featureToggles.scopeFilters = true;
config.featureToggles.passScopeToDashboardApi = false;
});
beforeEach(() => {
setDashboardAPI(undefined);
locationService.push('/?scopes=scope1&scopes=scope2&scopes=scope3');
});
afterEach(() => {
resetScenes();
cleanup();
});
it('Legacy API should not pass the scopes', async () => {
config.featureToggles.kubernetesDashboards = false;
await getDashboardAPI().getDashboardDTO('1');
expect(getMock).toHaveBeenCalledWith('/api/dashboards/uid/1', undefined);
});
it('K8s API should not pass the scopes', async () => {
config.featureToggles.kubernetesDashboards = true;
await getDashboardAPI().getDashboardDTO('1');
expect(getMock).toHaveBeenCalledWith(
'/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/1/dto'
);
});
});
describe('Feature flag on', () => {
beforeAll(() => {
config.featureToggles.scopeFilters = true;
config.featureToggles.passScopeToDashboardApi = true;
});
beforeEach(() => {
setDashboardAPI(undefined);
locationService.push('/?scopes=scope1&scopes=scope2&scopes=scope3');
initializeScopes();
});
afterEach(() => {
resetScenes();
cleanup();
});
it('Legacy API should pass the scopes', async () => {
config.featureToggles.kubernetesDashboards = false;
await getDashboardAPI().getDashboardDTO('1');
expect(getMock).toHaveBeenCalledWith('/api/dashboards/uid/1', { scopes: ['scope1', 'scope2', 'scope3'] });
});
it('K8s API should not pass the scopes', async () => {
config.featureToggles.kubernetesDashboards = true;
await getDashboardAPI().getDashboardDTO('1');
expect(getMock).toHaveBeenCalledWith(
'/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/1/dto'
);
});
});
});
});

View File

@ -0,0 +1,687 @@
import { act, cleanup, waitFor } from '@testing-library/react';
import userEvents from '@testing-library/user-event';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { config, setPluginImportUtils } from '@grafana/runtime';
import { sceneGraph } from '@grafana/scenes';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
import { initializeScopes, scopesDashboardsScene, scopesSelectorScene } from '../instance';
import { getClosestScopesFacade } from '../utils';
import {
fetchDashboardsSpy,
fetchNodesSpy,
fetchScopeSpy,
fetchSelectedScopesSpy,
getMock,
mocksScopes,
} from './utils/mocks';
import { buildTestScene, renderDashboard, resetScenes } from './utils/render';
import {
getDashboard,
getDashboardFolderExpand,
getDashboardsExpand,
getDashboardsSearch,
getNotFoundForFilter,
getNotFoundForFilterClear,
getNotFoundForScope,
getNotFoundNoScopes,
getPersistedApplicationsMimirSelect,
getPersistedApplicationsMimirTitle,
getResultApplicationsCloudDevSelect,
getResultApplicationsCloudExpand,
getResultApplicationsCloudOpsSelect,
getResultApplicationsCloudSelect,
getResultApplicationsExpand,
getResultApplicationsGrafanaSelect,
getResultApplicationsGrafanaTitle,
getResultApplicationsMimirSelect,
getResultApplicationsMimirTitle,
getResultCloudDevRadio,
getResultCloudExpand,
getResultCloudOpsRadio,
getResultCloudSelect,
getSelectorApply,
getSelectorCancel,
getSelectorInput,
getTreeHeadline,
getTreeSearch,
queryAllDashboard,
queryDashboard,
queryDashboardFolderExpand,
queryDashboardsContainer,
queryDashboardsSearch,
queryPersistedApplicationsGrafanaTitle,
queryPersistedApplicationsMimirTitle,
queryResultApplicationsCloudTitle,
queryResultApplicationsGrafanaTitle,
queryResultApplicationsMimirTitle,
querySelectorApply,
} from './utils/selectors';
jest.mock('@grafana/runtime', () => ({
__esModule: true,
...jest.requireActual('@grafana/runtime'),
useChromeHeaderHeight: jest.fn(),
getBackendSrv: () => ({
get: getMock,
}),
usePluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }),
}));
const panelPlugin = getPanelPlugin({
id: 'table',
skipDataQuery: true,
});
config.panels['table'] = panelPlugin.meta;
setPluginImportUtils({
importPanelPlugin: () => Promise.resolve(panelPlugin),
getPanelPluginFromCache: () => undefined,
});
describe('Scopes', () => {
describe('Feature flag off', () => {
beforeAll(() => {
config.featureToggles.scopeFilters = false;
config.featureToggles.groupByVariable = true;
initializeScopes();
});
it('Does not initialize', () => {
const dashboardScene = buildTestScene();
dashboardScene.activate();
expect(scopesSelectorScene).toBeNull();
});
});
describe('Feature flag on', () => {
let dashboardScene: DashboardScene;
beforeAll(() => {
config.featureToggles.scopeFilters = true;
config.featureToggles.groupByVariable = true;
});
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation(jest.fn());
fetchNodesSpy.mockClear();
fetchScopeSpy.mockClear();
fetchSelectedScopesSpy.mockClear();
fetchDashboardsSpy.mockClear();
getMock.mockClear();
initializeScopes();
dashboardScene = buildTestScene();
renderDashboard(dashboardScene);
});
afterEach(() => {
resetScenes();
cleanup();
});
describe('Tree', () => {
it('Navigates through scopes nodes', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsCloudExpand());
await userEvents.click(getResultApplicationsExpand());
});
it('Fetches scope details on select', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsGrafanaSelect());
await waitFor(() => expect(fetchScopeSpy).toHaveBeenCalledTimes(1));
});
it('Selects the proper scopes', async () => {
await act(async () =>
scopesSelectorScene?.updateScopes([
{ scopeName: 'grafana', path: [] },
{ scopeName: 'mimir', path: [] },
])
);
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
expect(getResultApplicationsGrafanaSelect()).toBeChecked();
expect(getResultApplicationsMimirSelect()).toBeChecked();
});
it('Can select scopes from same level', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsGrafanaSelect());
await userEvents.click(getResultApplicationsMimirSelect());
await userEvents.click(getResultApplicationsCloudSelect());
await userEvents.click(getSelectorApply());
expect(getSelectorInput().value).toBe('Grafana, Mimir, Cloud');
});
it('Can select a node from an inner level', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsGrafanaSelect());
await userEvents.click(getResultApplicationsCloudExpand());
await userEvents.click(getResultApplicationsCloudDevSelect());
await userEvents.click(getSelectorApply());
expect(getSelectorInput().value).toBe('Dev');
});
it('Can select a node from an upper level', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsGrafanaSelect());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultCloudSelect());
await userEvents.click(getSelectorApply());
expect(getSelectorInput().value).toBe('Cloud');
});
it('Respects only one select per container', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultCloudExpand());
await userEvents.click(getResultCloudDevRadio());
expect(getResultCloudDevRadio().checked).toBe(true);
expect(getResultCloudOpsRadio().checked).toBe(false);
await userEvents.click(getResultCloudOpsRadio());
expect(getResultCloudDevRadio().checked).toBe(false);
expect(getResultCloudOpsRadio().checked).toBe(true);
});
it('Search works', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.type(getTreeSearch(), 'Cloud');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
expect(queryResultApplicationsGrafanaTitle()).not.toBeInTheDocument();
expect(queryResultApplicationsMimirTitle()).not.toBeInTheDocument();
expect(getResultApplicationsCloudSelect()).toBeInTheDocument();
await userEvents.clear(getTreeSearch());
await userEvents.type(getTreeSearch(), 'Grafana');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(4));
expect(getResultApplicationsGrafanaSelect()).toBeInTheDocument();
expect(queryResultApplicationsCloudTitle()).not.toBeInTheDocument();
});
it('Opens to a selected scope', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsMimirSelect());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultCloudExpand());
await userEvents.click(getSelectorApply());
await userEvents.click(getSelectorInput());
expect(queryResultApplicationsMimirTitle()).toBeInTheDocument();
});
it('Persists a scope', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsMimirSelect());
await userEvents.type(getTreeSearch(), 'grafana');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
expect(getPersistedApplicationsMimirTitle()).toBeInTheDocument();
expect(queryPersistedApplicationsGrafanaTitle()).not.toBeInTheDocument();
expect(queryResultApplicationsMimirTitle()).not.toBeInTheDocument();
expect(getResultApplicationsGrafanaTitle()).toBeInTheDocument();
});
it('Does not persist a retrieved scope', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsMimirSelect());
await userEvents.type(getTreeSearch(), 'mimir');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
expect(queryPersistedApplicationsMimirTitle()).not.toBeInTheDocument();
expect(getResultApplicationsMimirTitle()).toBeInTheDocument();
});
it('Removes persisted nodes', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsMimirSelect());
await userEvents.type(getTreeSearch(), 'grafana');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
await userEvents.clear(getTreeSearch());
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(4));
expect(queryPersistedApplicationsMimirTitle()).not.toBeInTheDocument();
expect(queryPersistedApplicationsGrafanaTitle()).not.toBeInTheDocument();
expect(getResultApplicationsMimirTitle()).toBeInTheDocument();
expect(getResultApplicationsGrafanaTitle()).toBeInTheDocument();
});
it('Persists nodes from search', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.type(getTreeSearch(), 'mimir');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
await userEvents.click(getResultApplicationsMimirSelect());
await userEvents.type(getTreeSearch(), 'unknown');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(4));
expect(getPersistedApplicationsMimirTitle()).toBeInTheDocument();
await userEvents.clear(getTreeSearch());
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(5));
expect(getResultApplicationsMimirTitle()).toBeInTheDocument();
expect(getResultApplicationsGrafanaTitle()).toBeInTheDocument();
});
it('Selects a persisted scope', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsMimirSelect());
await userEvents.type(getTreeSearch(), 'grafana');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
await userEvents.click(getResultApplicationsGrafanaSelect());
await userEvents.click(getSelectorApply());
expect(getSelectorInput().value).toBe('Mimir, Grafana');
});
it('Deselects a persisted scope', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsMimirSelect());
await userEvents.type(getTreeSearch(), 'grafana');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
await userEvents.click(getResultApplicationsGrafanaSelect());
await userEvents.click(getSelectorApply());
expect(getSelectorInput().value).toBe('Mimir, Grafana');
await userEvents.click(getSelectorInput());
await userEvents.click(getPersistedApplicationsMimirSelect());
await userEvents.click(getSelectorApply());
expect(getSelectorInput().value).toBe('Grafana');
});
it('Shows the proper headline', async () => {
await userEvents.click(getSelectorInput());
expect(getTreeHeadline()).toHaveTextContent('Recommended');
await userEvents.type(getTreeSearch(), 'Applications');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(2));
expect(getTreeHeadline()).toHaveTextContent('Results');
await userEvents.type(getTreeSearch(), 'unknown');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
expect(getTreeHeadline()).toHaveTextContent('No results found for your query');
});
});
describe('Selector', () => {
it('Opens', async () => {
await userEvents.click(getSelectorInput());
expect(getSelectorApply()).toBeInTheDocument();
});
it('Fetches scope details on save', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultCloudSelect());
await userEvents.click(getSelectorApply());
await waitFor(() => expect(fetchSelectedScopesSpy).toHaveBeenCalled());
expect(getClosestScopesFacade(dashboardScene)?.value).toEqual(
mocksScopes.filter(({ metadata: { name } }) => name === 'cloud')
);
});
it('Does not save the scopes on close', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultCloudSelect());
await userEvents.click(getSelectorCancel());
await waitFor(() => expect(fetchSelectedScopesSpy).not.toHaveBeenCalled());
expect(getClosestScopesFacade(dashboardScene)?.value).toEqual([]);
});
it('Shows selected scopes', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultCloudSelect());
await userEvents.click(getSelectorApply());
expect(getSelectorInput().value).toEqual('Cloud');
});
});
describe('Dashboards list', () => {
it('Toggles expanded state', async () => {
await userEvents.click(getDashboardsExpand());
expect(getNotFoundNoScopes()).toBeInTheDocument();
});
it('Does not fetch dashboards list when the list is not expanded', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsMimirSelect());
await userEvents.click(getSelectorApply());
await waitFor(() => expect(fetchDashboardsSpy).not.toHaveBeenCalled());
});
it('Fetches dashboards list when the list is expanded', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsMimirSelect());
await userEvents.click(getSelectorApply());
await waitFor(() => expect(fetchDashboardsSpy).toHaveBeenCalled());
});
it('Fetches dashboards list when the list is expanded after scope selection', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsMimirSelect());
await userEvents.click(getSelectorApply());
await userEvents.click(getDashboardsExpand());
await waitFor(() => expect(fetchDashboardsSpy).toHaveBeenCalled());
});
it('Shows dashboards for multiple scopes', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsGrafanaSelect());
await userEvents.click(getSelectorApply());
await userEvents.click(getDashboardFolderExpand('General'));
await userEvents.click(getDashboardFolderExpand('Observability'));
await userEvents.click(getDashboardFolderExpand('Usage'));
expect(queryDashboardFolderExpand('Components')).not.toBeInTheDocument();
expect(queryDashboardFolderExpand('Investigations')).not.toBeInTheDocument();
expect(getDashboard('general-data-sources')).toBeInTheDocument();
expect(getDashboard('general-usage')).toBeInTheDocument();
expect(getDashboard('observability-backend-errors')).toBeInTheDocument();
expect(getDashboard('observability-backend-logs')).toBeInTheDocument();
expect(getDashboard('observability-frontend-errors')).toBeInTheDocument();
expect(getDashboard('observability-frontend-logs')).toBeInTheDocument();
expect(getDashboard('usage-data-sources')).toBeInTheDocument();
expect(getDashboard('usage-stats')).toBeInTheDocument();
expect(getDashboard('usage-usage-overview')).toBeInTheDocument();
expect(getDashboard('frontend')).toBeInTheDocument();
expect(getDashboard('overview')).toBeInTheDocument();
expect(getDashboard('stats')).toBeInTheDocument();
expect(queryDashboard('multiple3-datasource-errors')).not.toBeInTheDocument();
expect(queryDashboard('multiple4-datasource-logs')).not.toBeInTheDocument();
expect(queryDashboard('multiple0-ingester')).not.toBeInTheDocument();
expect(queryDashboard('multiple1-distributor')).not.toBeInTheDocument();
expect(queryDashboard('multiple2-compacter')).not.toBeInTheDocument();
expect(queryDashboard('another-stats')).not.toBeInTheDocument();
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsMimirSelect());
await userEvents.click(getSelectorApply());
await userEvents.click(getDashboardFolderExpand('General'));
await userEvents.click(getDashboardFolderExpand('Observability'));
await userEvents.click(getDashboardFolderExpand('Usage'));
await userEvents.click(getDashboardFolderExpand('Components'));
await userEvents.click(getDashboardFolderExpand('Investigations'));
expect(getDashboard('general-data-sources')).toBeInTheDocument();
expect(getDashboard('general-usage')).toBeInTheDocument();
expect(getDashboard('observability-backend-errors')).toBeInTheDocument();
expect(getDashboard('observability-backend-logs')).toBeInTheDocument();
expect(getDashboard('observability-frontend-errors')).toBeInTheDocument();
expect(getDashboard('observability-frontend-logs')).toBeInTheDocument();
expect(getDashboard('usage-data-sources')).toBeInTheDocument();
expect(getDashboard('usage-stats')).toBeInTheDocument();
expect(getDashboard('usage-usage-overview')).toBeInTheDocument();
expect(getDashboard('frontend')).toBeInTheDocument();
expect(getDashboard('overview')).toBeInTheDocument();
expect(getDashboard('stats')).toBeInTheDocument();
expect(queryAllDashboard('multiple3-datasource-errors')).toHaveLength(2);
expect(queryAllDashboard('multiple4-datasource-logs')).toHaveLength(2);
expect(queryAllDashboard('multiple0-ingester')).toHaveLength(2);
expect(queryAllDashboard('multiple1-distributor')).toHaveLength(2);
expect(queryAllDashboard('multiple2-compacter')).toHaveLength(2);
expect(getDashboard('another-stats')).toBeInTheDocument();
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsMimirSelect());
await userEvents.click(getSelectorApply());
await userEvents.click(getDashboardFolderExpand('General'));
await userEvents.click(getDashboardFolderExpand('Observability'));
await userEvents.click(getDashboardFolderExpand('Usage'));
expect(queryDashboardFolderExpand('Components')).not.toBeInTheDocument();
expect(queryDashboardFolderExpand('Investigations')).not.toBeInTheDocument();
expect(getDashboard('general-data-sources')).toBeInTheDocument();
expect(getDashboard('general-usage')).toBeInTheDocument();
expect(getDashboard('observability-backend-errors')).toBeInTheDocument();
expect(getDashboard('observability-backend-logs')).toBeInTheDocument();
expect(getDashboard('observability-frontend-errors')).toBeInTheDocument();
expect(getDashboard('observability-frontend-logs')).toBeInTheDocument();
expect(getDashboard('usage-data-sources')).toBeInTheDocument();
expect(getDashboard('usage-stats')).toBeInTheDocument();
expect(getDashboard('usage-usage-overview')).toBeInTheDocument();
expect(getDashboard('frontend')).toBeInTheDocument();
expect(getDashboard('overview')).toBeInTheDocument();
expect(getDashboard('stats')).toBeInTheDocument();
expect(queryDashboard('multiple3-datasource-errors')).not.toBeInTheDocument();
expect(queryDashboard('multiple4-datasource-logs')).not.toBeInTheDocument();
expect(queryDashboard('multiple0-ingester')).not.toBeInTheDocument();
expect(queryDashboard('multiple1-distributor')).not.toBeInTheDocument();
expect(queryDashboard('multiple2-compacter')).not.toBeInTheDocument();
expect(queryDashboard('another-stats')).not.toBeInTheDocument();
});
it('Filters the dashboards list for dashboards', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsGrafanaSelect());
await userEvents.click(getSelectorApply());
await userEvents.click(getDashboardFolderExpand('General'));
await userEvents.click(getDashboardFolderExpand('Observability'));
await userEvents.click(getDashboardFolderExpand('Usage'));
expect(getDashboard('general-data-sources')).toBeInTheDocument();
expect(getDashboard('general-usage')).toBeInTheDocument();
expect(getDashboard('observability-backend-errors')).toBeInTheDocument();
expect(getDashboard('observability-backend-logs')).toBeInTheDocument();
expect(getDashboard('observability-frontend-errors')).toBeInTheDocument();
expect(getDashboard('observability-frontend-logs')).toBeInTheDocument();
expect(getDashboard('usage-data-sources')).toBeInTheDocument();
expect(getDashboard('usage-stats')).toBeInTheDocument();
expect(getDashboard('usage-usage-overview')).toBeInTheDocument();
expect(getDashboard('frontend')).toBeInTheDocument();
expect(getDashboard('overview')).toBeInTheDocument();
expect(getDashboard('stats')).toBeInTheDocument();
await userEvents.type(getDashboardsSearch(), 'Stats');
await waitFor(() => {
expect(queryDashboard('general-data-sources')).not.toBeInTheDocument();
expect(queryDashboard('general-usage')).not.toBeInTheDocument();
expect(queryDashboard('observability-backend-errors')).not.toBeInTheDocument();
expect(queryDashboard('observability-backend-logs')).not.toBeInTheDocument();
expect(queryDashboard('observability-frontend-errors')).not.toBeInTheDocument();
expect(queryDashboard('observability-frontend-logs')).not.toBeInTheDocument();
expect(queryDashboard('usage-data-sources')).not.toBeInTheDocument();
expect(getDashboard('usage-stats')).toBeInTheDocument();
expect(queryDashboard('usage-usage-overview')).not.toBeInTheDocument();
expect(queryDashboard('frontend')).not.toBeInTheDocument();
expect(queryDashboard('overview')).not.toBeInTheDocument();
expect(getDashboard('stats')).toBeInTheDocument();
});
});
it('Filters the dashboards list for folders', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsGrafanaSelect());
await userEvents.click(getSelectorApply());
await userEvents.click(getDashboardFolderExpand('General'));
await userEvents.click(getDashboardFolderExpand('Observability'));
await userEvents.click(getDashboardFolderExpand('Usage'));
expect(getDashboard('general-data-sources')).toBeInTheDocument();
expect(getDashboard('general-usage')).toBeInTheDocument();
expect(getDashboard('observability-backend-errors')).toBeInTheDocument();
expect(getDashboard('observability-backend-logs')).toBeInTheDocument();
expect(getDashboard('observability-frontend-errors')).toBeInTheDocument();
expect(getDashboard('observability-frontend-logs')).toBeInTheDocument();
expect(getDashboard('usage-data-sources')).toBeInTheDocument();
expect(getDashboard('usage-stats')).toBeInTheDocument();
expect(getDashboard('usage-usage-overview')).toBeInTheDocument();
expect(getDashboard('frontend')).toBeInTheDocument();
expect(getDashboard('overview')).toBeInTheDocument();
expect(getDashboard('stats')).toBeInTheDocument();
await userEvents.type(getDashboardsSearch(), 'Usage');
await waitFor(() => {
expect(queryDashboard('general-data-sources')).not.toBeInTheDocument();
expect(getDashboard('general-usage')).toBeInTheDocument();
expect(queryDashboard('observability-backend-errors')).not.toBeInTheDocument();
expect(queryDashboard('observability-backend-logs')).not.toBeInTheDocument();
expect(queryDashboard('observability-frontend-errors')).not.toBeInTheDocument();
expect(queryDashboard('observability-frontend-logs')).not.toBeInTheDocument();
expect(getDashboard('usage-data-sources')).toBeInTheDocument();
expect(getDashboard('usage-stats')).toBeInTheDocument();
expect(getDashboard('usage-usage-overview')).toBeInTheDocument();
expect(queryDashboard('frontend')).not.toBeInTheDocument();
expect(queryDashboard('overview')).not.toBeInTheDocument();
expect(queryDashboard('stats')).not.toBeInTheDocument();
});
});
it('Deduplicates the dashboards list', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsCloudExpand());
await userEvents.click(getResultApplicationsCloudDevSelect());
await userEvents.click(getResultApplicationsCloudOpsSelect());
await userEvents.click(getSelectorApply());
await userEvents.click(getDashboardFolderExpand('Cardinality Management'));
await userEvents.click(getDashboardFolderExpand('Usage Insights'));
expect(queryAllDashboard('cardinality-management-labels')).toHaveLength(1);
expect(queryAllDashboard('cardinality-management-metrics')).toHaveLength(1);
expect(queryAllDashboard('cardinality-management-overview')).toHaveLength(1);
expect(queryAllDashboard('usage-insights-alertmanager')).toHaveLength(1);
expect(queryAllDashboard('usage-insights-data-sources')).toHaveLength(1);
expect(queryAllDashboard('usage-insights-metrics-ingestion')).toHaveLength(1);
expect(queryAllDashboard('usage-insights-overview')).toHaveLength(1);
expect(queryAllDashboard('usage-insights-query-errors')).toHaveLength(1);
expect(queryAllDashboard('billing-usage')).toHaveLength(1);
});
it('Shows a proper message when no scopes are selected', async () => {
await userEvents.click(getDashboardsExpand());
expect(getNotFoundNoScopes()).toBeInTheDocument();
expect(queryDashboardsSearch()).not.toBeInTheDocument();
});
it('Does not show the input when there are no dashboards found for scope', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getSelectorInput());
await userEvents.click(getResultCloudSelect());
await userEvents.click(getSelectorApply());
expect(getNotFoundForScope()).toBeInTheDocument();
expect(queryDashboardsSearch()).not.toBeInTheDocument();
});
it('Shows the input and a message when there are no dashboards found for filter', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsMimirSelect());
await userEvents.click(getSelectorApply());
await userEvents.type(getDashboardsSearch(), 'unknown');
await waitFor(() => {
expect(queryDashboardsSearch()).toBeInTheDocument();
expect(getNotFoundForFilter()).toBeInTheDocument();
});
await userEvents.click(getNotFoundForFilterClear());
await waitFor(() => {
expect(getDashboardsSearch().value).toBe('');
});
});
});
describe('View mode', () => {
it('Enters view mode', async () => {
await act(async () => dashboardScene.onEnterEditMode());
expect(scopesSelectorScene?.state?.isReadOnly).toEqual(true);
expect(scopesDashboardsScene?.state?.isPanelOpened).toEqual(false);
});
it('Closes selector on enter', async () => {
await userEvents.click(getSelectorInput());
await act(async () => dashboardScene.onEnterEditMode());
expect(querySelectorApply()).not.toBeInTheDocument();
});
it('Closes dashboards list on enter', async () => {
await userEvents.click(getDashboardsExpand());
await act(async () => dashboardScene.onEnterEditMode());
expect(queryDashboardsContainer()).not.toBeInTheDocument();
});
it('Does not open selector when view mode is active', async () => {
await act(async () => dashboardScene.onEnterEditMode());
await userEvents.click(getSelectorInput());
expect(querySelectorApply()).not.toBeInTheDocument();
});
it('Disables the expand button when view mode is active', async () => {
await act(async () => dashboardScene.onEnterEditMode());
expect(getDashboardsExpand()).toBeDisabled();
});
});
describe('Enrichers', () => {
it('Data requests', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsGrafanaSelect());
await userEvents.click(getSelectorApply());
await waitFor(() => {
const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!;
expect(dashboardScene.enrichDataRequest(queryRunner).scopes).toEqual(
mocksScopes.filter(({ metadata: { name } }) => name === 'grafana')
);
});
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsMimirSelect());
await userEvents.click(getSelectorApply());
await waitFor(() => {
const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!;
expect(dashboardScene.enrichDataRequest(queryRunner).scopes).toEqual(
mocksScopes.filter(({ metadata: { name } }) => name === 'grafana' || name === 'mimir')
);
});
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsGrafanaSelect());
await userEvents.click(getSelectorApply());
await waitFor(() => {
const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!;
expect(dashboardScene.enrichDataRequest(queryRunner).scopes).toEqual(
mocksScopes.filter(({ metadata: { name } }) => name === 'mimir')
);
});
});
it('Filters requests', async () => {
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsExpand());
await userEvents.click(getResultApplicationsGrafanaSelect());
await userEvents.click(getSelectorApply());
await waitFor(() => {
expect(dashboardScene.enrichFiltersRequest().scopes).toEqual(
mocksScopes.filter(({ metadata: { name } }) => name === 'grafana')
);
});
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsMimirSelect());
await userEvents.click(getSelectorApply());
await waitFor(() => {
expect(dashboardScene.enrichFiltersRequest().scopes).toEqual(
mocksScopes.filter(({ metadata: { name } }) => name === 'grafana' || name === 'mimir')
);
});
await userEvents.click(getSelectorInput());
await userEvents.click(getResultApplicationsGrafanaSelect());
await userEvents.click(getSelectorApply());
await waitFor(() => {
expect(dashboardScene.enrichFiltersRequest().scopes).toEqual(
mocksScopes.filter(({ metadata: { name } }) => name === 'mimir')
);
});
});
});
});
});

View File

@ -0,0 +1,366 @@
import { filterFolders, groupDashboards } from '../internal/utils';
import {
alternativeDashboardWithRootFolder,
alternativeDashboardWithTwoFolders,
dashboardWithOneFolder,
dashboardWithoutFolder,
dashboardWithRootFolder,
dashboardWithRootFolderAndOtherFolder,
dashboardWithTwoFolders,
} from './utils/mocks';
describe('Scopes', () => {
describe('Utils', () => {
describe('groupDashboards', () => {
it('Assigns dashboards without groups to root folder', () => {
expect(groupDashboards([dashboardWithoutFolder])).toEqual({
'': {
title: '',
isExpanded: true,
folders: {},
dashboards: {
[dashboardWithoutFolder.spec.dashboard]: {
dashboard: dashboardWithoutFolder.spec.dashboard,
dashboardTitle: dashboardWithoutFolder.spec.dashboardTitle,
items: [dashboardWithoutFolder],
},
},
},
});
});
it('Assigns dashboards with root group to root folder', () => {
expect(groupDashboards([dashboardWithRootFolder])).toEqual({
'': {
title: '',
isExpanded: true,
folders: {},
dashboards: {
[dashboardWithRootFolder.spec.dashboard]: {
dashboard: dashboardWithRootFolder.spec.dashboard,
dashboardTitle: dashboardWithRootFolder.spec.dashboardTitle,
items: [dashboardWithRootFolder],
},
},
},
});
});
it('Merges folders from multiple dashboards', () => {
expect(groupDashboards([dashboardWithOneFolder, dashboardWithTwoFolders])).toEqual({
'': {
title: '',
isExpanded: true,
folders: {
'Folder 1': {
title: 'Folder 1',
isExpanded: false,
folders: {},
dashboards: {
[dashboardWithOneFolder.spec.dashboard]: {
dashboard: dashboardWithOneFolder.spec.dashboard,
dashboardTitle: dashboardWithOneFolder.spec.dashboardTitle,
items: [dashboardWithOneFolder],
},
[dashboardWithTwoFolders.spec.dashboard]: {
dashboard: dashboardWithTwoFolders.spec.dashboard,
dashboardTitle: dashboardWithTwoFolders.spec.dashboardTitle,
items: [dashboardWithTwoFolders],
},
},
},
'Folder 2': {
title: 'Folder 2',
isExpanded: false,
folders: {},
dashboards: {
[dashboardWithTwoFolders.spec.dashboard]: {
dashboard: dashboardWithTwoFolders.spec.dashboard,
dashboardTitle: dashboardWithTwoFolders.spec.dashboardTitle,
items: [dashboardWithTwoFolders],
},
},
},
},
dashboards: {},
},
});
});
it('Merges scopes from multiple dashboards', () => {
expect(groupDashboards([dashboardWithTwoFolders, alternativeDashboardWithTwoFolders])).toEqual({
'': {
title: '',
isExpanded: true,
folders: {
'Folder 1': {
title: 'Folder 1',
isExpanded: false,
folders: {},
dashboards: {
[dashboardWithTwoFolders.spec.dashboard]: {
dashboard: dashboardWithTwoFolders.spec.dashboard,
dashboardTitle: dashboardWithTwoFolders.spec.dashboardTitle,
items: [dashboardWithTwoFolders, alternativeDashboardWithTwoFolders],
},
},
},
'Folder 2': {
title: 'Folder 2',
isExpanded: false,
folders: {},
dashboards: {
[dashboardWithTwoFolders.spec.dashboard]: {
dashboard: dashboardWithTwoFolders.spec.dashboard,
dashboardTitle: dashboardWithTwoFolders.spec.dashboardTitle,
items: [dashboardWithTwoFolders, alternativeDashboardWithTwoFolders],
},
},
},
},
dashboards: {},
},
});
});
it('Matches snapshot', () => {
expect(
groupDashboards([
dashboardWithoutFolder,
dashboardWithOneFolder,
dashboardWithTwoFolders,
alternativeDashboardWithTwoFolders,
dashboardWithRootFolder,
alternativeDashboardWithRootFolder,
dashboardWithRootFolderAndOtherFolder,
])
).toEqual({
'': {
dashboards: {
[dashboardWithRootFolderAndOtherFolder.spec.dashboard]: {
dashboard: dashboardWithRootFolderAndOtherFolder.spec.dashboard,
dashboardTitle: dashboardWithRootFolderAndOtherFolder.spec.dashboardTitle,
items: [dashboardWithRootFolderAndOtherFolder],
},
[dashboardWithRootFolder.spec.dashboard]: {
dashboard: dashboardWithRootFolder.spec.dashboard,
dashboardTitle: dashboardWithRootFolder.spec.dashboardTitle,
items: [dashboardWithRootFolder, alternativeDashboardWithRootFolder],
},
[dashboardWithoutFolder.spec.dashboard]: {
dashboard: dashboardWithoutFolder.spec.dashboard,
dashboardTitle: dashboardWithoutFolder.spec.dashboardTitle,
items: [dashboardWithoutFolder],
},
},
folders: {
'Folder 1': {
dashboards: {
[dashboardWithOneFolder.spec.dashboard]: {
dashboard: dashboardWithOneFolder.spec.dashboard,
dashboardTitle: dashboardWithOneFolder.spec.dashboardTitle,
items: [dashboardWithOneFolder],
},
[dashboardWithTwoFolders.spec.dashboard]: {
dashboard: dashboardWithTwoFolders.spec.dashboard,
dashboardTitle: dashboardWithTwoFolders.spec.dashboardTitle,
items: [dashboardWithTwoFolders, alternativeDashboardWithTwoFolders],
},
},
folders: {},
isExpanded: false,
title: 'Folder 1',
},
'Folder 2': {
dashboards: {
[dashboardWithTwoFolders.spec.dashboard]: {
dashboard: dashboardWithTwoFolders.spec.dashboard,
dashboardTitle: dashboardWithTwoFolders.spec.dashboardTitle,
items: [dashboardWithTwoFolders, alternativeDashboardWithTwoFolders],
},
},
folders: {},
isExpanded: false,
title: 'Folder 2',
},
'Folder 3': {
dashboards: {
[dashboardWithRootFolderAndOtherFolder.spec.dashboard]: {
dashboard: dashboardWithRootFolderAndOtherFolder.spec.dashboard,
dashboardTitle: dashboardWithRootFolderAndOtherFolder.spec.dashboardTitle,
items: [dashboardWithRootFolderAndOtherFolder],
},
},
folders: {},
isExpanded: false,
title: 'Folder 3',
},
},
isExpanded: true,
title: '',
},
});
});
});
describe('filterFolders', () => {
it('Shows folders matching criteria', () => {
expect(
filterFolders(
{
'': {
title: '',
isExpanded: true,
folders: {
'Folder 1': {
title: 'Folder 1',
isExpanded: false,
folders: {},
dashboards: {
'Dashboard ID': {
dashboard: 'Dashboard ID',
dashboardTitle: 'Dashboard Title',
items: [],
},
},
},
'Folder 2': {
title: 'Folder 2',
isExpanded: true,
folders: {},
dashboards: {
'Dashboard ID': {
dashboard: 'Dashboard ID',
dashboardTitle: 'Dashboard Title',
items: [],
},
},
},
},
dashboards: {
'Dashboard ID': {
dashboard: 'Dashboard ID',
dashboardTitle: 'Dashboard Title',
items: [],
},
},
},
},
'Folder'
)
).toEqual({
'': {
title: '',
isExpanded: true,
folders: {
'Folder 1': {
title: 'Folder 1',
isExpanded: true,
folders: {},
dashboards: {
'Dashboard ID': {
dashboard: 'Dashboard ID',
dashboardTitle: 'Dashboard Title',
items: [],
},
},
},
'Folder 2': {
title: 'Folder 2',
isExpanded: true,
folders: {},
dashboards: {
'Dashboard ID': {
dashboard: 'Dashboard ID',
dashboardTitle: 'Dashboard Title',
items: [],
},
},
},
},
dashboards: {},
},
});
});
it('Shows dashboards matching criteria', () => {
expect(
filterFolders(
{
'': {
title: '',
isExpanded: true,
folders: {
'Folder 1': {
title: 'Folder 1',
isExpanded: false,
folders: {},
dashboards: {
'Dashboard ID': {
dashboard: 'Dashboard ID',
dashboardTitle: 'Dashboard Title',
items: [],
},
},
},
'Folder 2': {
title: 'Folder 2',
isExpanded: true,
folders: {},
dashboards: {
'Random ID': {
dashboard: 'Random ID',
dashboardTitle: 'Random Title',
items: [],
},
},
},
},
dashboards: {
'Dashboard ID': {
dashboard: 'Dashboard ID',
dashboardTitle: 'Dashboard Title',
items: [],
},
'Random ID': {
dashboard: 'Random ID',
dashboardTitle: 'Random Title',
items: [],
},
},
},
},
'dash'
)
).toEqual({
'': {
title: '',
isExpanded: true,
folders: {
'Folder 1': {
title: 'Folder 1',
isExpanded: true,
folders: {},
dashboards: {
'Dashboard ID': {
dashboard: 'Dashboard ID',
dashboardTitle: 'Dashboard Title',
items: [],
},
},
},
},
dashboards: {
'Dashboard ID': {
dashboard: 'Dashboard ID',
dashboardTitle: 'Dashboard Title',
items: [],
},
},
},
});
});
});
});
});

View File

@ -0,0 +1,446 @@
import { Scope, ScopeDashboardBinding, ScopeNode } from '@grafana/data';
import * as api from '../../internal/api';
export const mocksScopes: Scope[] = [
{
metadata: { name: 'cloud' },
spec: {
title: 'Cloud',
type: 'indexHelper',
description: 'redundant label filter but makes queries faster',
category: 'indexHelpers',
filters: [{ key: 'cloud', value: '.*', operator: 'regex-match' }],
},
},
{
metadata: { name: 'dev' },
spec: {
title: 'Dev',
type: 'cloud',
description: 'Dev',
category: 'cloud',
filters: [{ key: 'cloud', value: 'dev', operator: 'equals' }],
},
},
{
metadata: { name: 'ops' },
spec: {
title: 'Ops',
type: 'cloud',
description: 'Ops',
category: 'cloud',
filters: [{ key: 'cloud', value: 'ops', operator: 'equals' }],
},
},
{
metadata: { name: 'prod' },
spec: {
title: 'Prod',
type: 'cloud',
description: 'Prod',
category: 'cloud',
filters: [{ key: 'cloud', value: 'prod', operator: 'equals' }],
},
},
{
metadata: { name: 'grafana' },
spec: {
title: 'Grafana',
type: 'app',
description: 'Grafana',
category: 'apps',
filters: [{ key: 'app', value: 'grafana', operator: 'equals' }],
},
},
{
metadata: { name: 'mimir' },
spec: {
title: 'Mimir',
type: 'app',
description: 'Mimir',
category: 'apps',
filters: [{ key: 'app', value: 'mimir', operator: 'equals' }],
},
},
{
metadata: { name: 'loki' },
spec: {
title: 'Loki',
type: 'app',
description: 'Loki',
category: 'apps',
filters: [{ key: 'app', value: 'loki', operator: 'equals' }],
},
},
{
metadata: { name: 'tempo' },
spec: {
title: 'Tempo',
type: 'app',
description: 'Tempo',
category: 'apps',
filters: [{ key: 'app', value: 'tempo', operator: 'equals' }],
},
},
] as const;
const dashboardBindingsGenerator = (
scopes: string[],
dashboards: Array<{ dashboardTitle: string; dashboardKey?: string; groups?: string[] }>
) =>
scopes.reduce<ScopeDashboardBinding[]>((scopeAcc, scopeTitle) => {
const scope = scopeTitle.toLowerCase().replaceAll(' ', '-').replaceAll('/', '-');
return [
...scopeAcc,
...dashboards.reduce<ScopeDashboardBinding[]>((acc, { dashboardTitle, groups, dashboardKey }, idx) => {
dashboardKey = dashboardKey ?? dashboardTitle.toLowerCase().replaceAll(' ', '-').replaceAll('/', '-');
const group = !groups
? ''
: groups.length === 1
? groups[0] === ''
? ''
: `${groups[0].toLowerCase().replaceAll(' ', '-').replaceAll('/', '-')}-`
: `multiple${idx}-`;
const dashboard = `${group}${dashboardKey}`;
return [
...acc,
{
metadata: { name: `${scope}-${dashboard}` },
spec: {
dashboard,
dashboardTitle,
scope,
groups,
},
},
];
}, []),
];
}, []);
export const mocksScopeDashboardBindings: ScopeDashboardBinding[] = [
...dashboardBindingsGenerator(
['Grafana'],
[
{ dashboardTitle: 'Data Sources', groups: ['General'] },
{ dashboardTitle: 'Usage', groups: ['General'] },
{ dashboardTitle: 'Frontend Errors', groups: ['Observability'] },
{ dashboardTitle: 'Frontend Logs', groups: ['Observability'] },
{ dashboardTitle: 'Backend Errors', groups: ['Observability'] },
{ dashboardTitle: 'Backend Logs', groups: ['Observability'] },
{ dashboardTitle: 'Usage Overview', groups: ['Usage'] },
{ dashboardTitle: 'Data Sources', groups: ['Usage'] },
{ dashboardTitle: 'Stats', groups: ['Usage'] },
{ dashboardTitle: 'Overview', groups: [''] },
{ dashboardTitle: 'Frontend' },
{ dashboardTitle: 'Stats' },
]
),
...dashboardBindingsGenerator(
['Loki', 'Tempo', 'Mimir'],
[
{ dashboardTitle: 'Ingester', groups: ['Components', 'Investigations'] },
{ dashboardTitle: 'Distributor', groups: ['Components', 'Investigations'] },
{ dashboardTitle: 'Compacter', groups: ['Components', 'Investigations'] },
{ dashboardTitle: 'Datasource Errors', groups: ['Observability', 'Investigations'] },
{ dashboardTitle: 'Datasource Logs', groups: ['Observability', 'Investigations'] },
{ dashboardTitle: 'Overview' },
{ dashboardTitle: 'Stats', dashboardKey: 'another-stats' },
]
),
...dashboardBindingsGenerator(
['Dev', 'Ops', 'Prod'],
[
{ dashboardTitle: 'Overview', groups: ['Cardinality Management'] },
{ dashboardTitle: 'Metrics', groups: ['Cardinality Management'] },
{ dashboardTitle: 'Labels', groups: ['Cardinality Management'] },
{ dashboardTitle: 'Overview', groups: ['Usage Insights'] },
{ dashboardTitle: 'Data Sources', groups: ['Usage Insights'] },
{ dashboardTitle: 'Query Errors', groups: ['Usage Insights'] },
{ dashboardTitle: 'Alertmanager', groups: ['Usage Insights'] },
{ dashboardTitle: 'Metrics Ingestion', groups: ['Usage Insights'] },
{ dashboardTitle: 'Billing/Usage' },
]
),
] as const;
export const mocksNodes: Array<ScopeNode & { parent: string }> = [
{
parent: '',
metadata: { name: 'applications' },
spec: {
nodeType: 'container',
title: 'Applications',
description: 'Application Scopes',
},
},
{
parent: '',
metadata: { name: 'cloud' },
spec: {
nodeType: 'container',
title: 'Cloud',
description: 'Cloud Scopes',
disableMultiSelect: true,
linkType: 'scope',
linkId: 'cloud',
},
},
{
parent: 'applications',
metadata: { name: 'applications-grafana' },
spec: {
nodeType: 'leaf',
title: 'Grafana',
description: 'Grafana',
linkType: 'scope',
linkId: 'grafana',
},
},
{
parent: 'applications',
metadata: { name: 'applications-mimir' },
spec: {
nodeType: 'leaf',
title: 'Mimir',
description: 'Mimir',
linkType: 'scope',
linkId: 'mimir',
},
},
{
parent: 'applications',
metadata: { name: 'applications-loki' },
spec: {
nodeType: 'leaf',
title: 'Loki',
description: 'Loki',
linkType: 'scope',
linkId: 'loki',
},
},
{
parent: 'applications',
metadata: { name: 'applications-tempo' },
spec: {
nodeType: 'leaf',
title: 'Tempo',
description: 'Tempo',
linkType: 'scope',
linkId: 'tempo',
},
},
{
parent: 'applications',
metadata: { name: 'applications-cloud' },
spec: {
nodeType: 'container',
title: 'Cloud',
description: 'Application/Cloud Scopes',
linkType: 'scope',
linkId: 'cloud',
},
},
{
parent: 'applications-cloud',
metadata: { name: 'applications-cloud-dev' },
spec: {
nodeType: 'leaf',
title: 'Dev',
description: 'Dev',
linkType: 'scope',
linkId: 'dev',
},
},
{
parent: 'applications-cloud',
metadata: { name: 'applications-cloud-ops' },
spec: {
nodeType: 'leaf',
title: 'Ops',
description: 'Ops',
linkType: 'scope',
linkId: 'ops',
},
},
{
parent: 'applications-cloud',
metadata: { name: 'applications-cloud-prod' },
spec: {
nodeType: 'leaf',
title: 'Prod',
description: 'Prod',
linkType: 'scope',
linkId: 'prod',
},
},
{
parent: 'cloud',
metadata: { name: 'cloud-dev' },
spec: {
nodeType: 'leaf',
title: 'Dev',
description: 'Dev',
linkType: 'scope',
linkId: 'dev',
},
},
{
parent: 'cloud',
metadata: { name: 'cloud-ops' },
spec: {
nodeType: 'leaf',
title: 'Ops',
description: 'Ops',
linkType: 'scope',
linkId: 'ops',
},
},
{
parent: 'cloud',
metadata: { name: 'cloud-prod' },
spec: {
nodeType: 'leaf',
title: 'Prod',
description: 'Prod',
linkType: 'scope',
linkId: 'prod',
},
},
{
parent: 'cloud',
metadata: { name: 'cloud-applications' },
spec: {
nodeType: 'container',
title: 'Applications',
description: 'Cloud/Application Scopes',
},
},
{
parent: 'cloud-applications',
metadata: { name: 'cloud-applications-grafana' },
spec: {
nodeType: 'leaf',
title: 'Grafana',
description: 'Grafana',
linkType: 'scope',
linkId: 'grafana',
},
},
{
parent: 'cloud-applications',
metadata: { name: 'cloud-applications-mimir' },
spec: {
nodeType: 'leaf',
title: 'Mimir',
description: 'Mimir',
linkType: 'scope',
linkId: 'mimir',
},
},
{
parent: 'cloud-applications',
metadata: { name: 'cloud-applications-loki' },
spec: {
nodeType: 'leaf',
title: 'Loki',
description: 'Loki',
linkType: 'scope',
linkId: 'loki',
},
},
{
parent: 'cloud-applications',
metadata: { name: 'cloud-applications-tempo' },
spec: {
nodeType: 'leaf',
title: 'Tempo',
description: 'Tempo',
linkType: 'scope',
linkId: 'tempo',
},
},
] as const;
export const fetchNodesSpy = jest.spyOn(api, 'fetchNodes');
export const fetchScopeSpy = jest.spyOn(api, 'fetchScope');
export const fetchSelectedScopesSpy = jest.spyOn(api, 'fetchSelectedScopes');
export const fetchDashboardsSpy = jest.spyOn(api, 'fetchDashboards');
export const getMock = jest
.fn()
.mockImplementation((url: string, params: { parent: string; scope: string[]; query?: string }) => {
if (url.startsWith('/apis/scope.grafana.app/v0alpha1/namespaces/default/find/scope_node_children')) {
return {
items: mocksNodes.filter(
({ parent, spec: { title } }) =>
parent === params.parent && title.toLowerCase().includes((params.query ?? '').toLowerCase())
),
};
}
if (url.startsWith('/apis/scope.grafana.app/v0alpha1/namespaces/default/scopes/')) {
const name = url.replace('/apis/scope.grafana.app/v0alpha1/namespaces/default/scopes/', '');
return mocksScopes.find((scope) => scope.metadata.name.toLowerCase() === name.toLowerCase()) ?? {};
}
if (url.startsWith('/apis/scope.grafana.app/v0alpha1/namespaces/default/find/scope_dashboard_bindings')) {
return {
items: mocksScopeDashboardBindings.filter(({ spec: { scope: bindingScope } }) =>
params.scope.includes(bindingScope)
),
};
}
if (url.startsWith('/api/dashboards/uid/')) {
return {};
}
if (url.startsWith('/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/')) {
return {
metadata: {
name: '1',
},
};
}
return {};
});
const generateScopeDashboardBinding = (dashboardTitle: string, groups?: string[], dashboardId?: string) => ({
metadata: { name: `${dashboardTitle}-name` },
spec: {
dashboard: `${dashboardId ?? dashboardTitle}-dashboard`,
dashboardTitle,
scope: `${dashboardTitle}-scope`,
groups,
},
});
export const dashboardWithoutFolder: ScopeDashboardBinding = generateScopeDashboardBinding('Without Folder');
export const dashboardWithOneFolder: ScopeDashboardBinding = generateScopeDashboardBinding('With one folder', [
'Folder 1',
]);
export const dashboardWithTwoFolders: ScopeDashboardBinding = generateScopeDashboardBinding('With two folders', [
'Folder 1',
'Folder 2',
]);
export const alternativeDashboardWithTwoFolders: ScopeDashboardBinding = generateScopeDashboardBinding(
'Alternative with two folders',
['Folder 1', 'Folder 2'],
'With two folders'
);
export const dashboardWithRootFolder: ScopeDashboardBinding = generateScopeDashboardBinding('With root folder', ['']);
export const alternativeDashboardWithRootFolder: ScopeDashboardBinding = generateScopeDashboardBinding(
'Alternative With root folder',
[''],
'With root folder'
);
export const dashboardWithRootFolderAndOtherFolder: ScopeDashboardBinding = generateScopeDashboardBinding(
'With root folder and other folder',
['', 'Folder 3']
);

View File

@ -0,0 +1,92 @@
import { KBarProvider } from 'kbar';
import { render } from 'test/test-utils';
import {
AdHocFiltersVariable,
behaviors,
GroupByVariable,
sceneGraph,
SceneGridItem,
SceneGridLayout,
SceneQueryRunner,
SceneTimeRange,
SceneVariableSet,
VizPanel,
} from '@grafana/scenes';
import { AppChrome } from 'app/core/components/AppChrome/AppChrome';
import { DashboardControls } from 'app/features/dashboard-scene/scene//DashboardControls';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
import { ScopesFacade } from '../../ScopesFacadeScene';
import { scopesDashboardsScene, scopesSelectorScene } from '../../instance';
import { getInitialDashboardsState } from '../../internal/ScopesDashboardsScene';
import { initialSelectorState } from '../../internal/ScopesSelectorScene';
import { DASHBOARDS_OPENED_KEY } from '../../internal/const';
export function buildTestScene(overrides: Partial<DashboardScene> = {}) {
return new DashboardScene({
title: 'hello',
uid: 'dash-1',
description: 'hello description',
tags: ['tag1', 'tag2'],
editable: true,
$timeRange: new SceneTimeRange({
timeZone: 'browser',
}),
controls: new DashboardControls({}),
$behaviors: [
new behaviors.CursorSync({}),
new ScopesFacade({
handler: (facade) => sceneGraph.getTimeRange(facade).onRefresh(),
}),
],
$variables: new SceneVariableSet({
variables: [
new AdHocFiltersVariable({
name: 'adhoc',
datasource: { uid: 'my-ds-uid' },
}),
new GroupByVariable({
name: 'groupby',
datasource: { uid: 'my-ds-uid' },
}),
],
}),
body: new SceneGridLayout({
children: [
new SceneGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 300,
height: 300,
body: new VizPanel({
title: 'Panel A',
key: 'panel-1',
pluginId: 'table',
$data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
}),
}),
],
}),
...overrides,
});
}
export function renderDashboard(dashboardScene: DashboardScene) {
return render(
<KBarProvider>
<AppChrome>
<dashboardScene.Component model={dashboardScene} />
</AppChrome>
</KBarProvider>
);
}
export function resetScenes() {
scopesSelectorScene?.setState(initialSelectorState);
localStorage.removeItem(DASHBOARDS_OPENED_KEY);
scopesDashboardsScene?.setState(getInitialDashboardsState());
}

View File

@ -0,0 +1,92 @@
import { screen } from '@testing-library/react';
const selectors = {
tree: {
search: 'scopes-tree-search',
headline: 'scopes-tree-headline',
select: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-checkbox`,
radio: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-radio`,
expand: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-expand`,
title: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-title`,
},
selector: {
input: 'scopes-selector-input',
container: 'scopes-selector-container',
loading: 'scopes-selector-loading',
apply: 'scopes-selector-apply',
cancel: 'scopes-selector-cancel',
},
dashboards: {
expand: 'scopes-dashboards-expand',
container: 'scopes-dashboards-container',
search: 'scopes-dashboards-search',
loading: 'scopes-dashboards-loading',
dashboard: (uid: string) => `scopes-dashboards-${uid}`,
dashboardExpand: (uid: string) => `scopes-dashboards-${uid}-expand`,
notFoundNoScopes: 'scopes-dashboards-notFoundNoScopes',
notFoundForScope: 'scopes-dashboards-notFoundForScope',
notFoundForFilter: 'scopes-dashboards-notFoundForFilter',
notFoundForFilterClear: 'scopes-dashboards-notFoundForFilter-clear',
},
};
export const getSelectorInput = () => screen.getByTestId<HTMLInputElement>(selectors.selector.input);
export const querySelectorApply = () => screen.queryByTestId(selectors.selector.apply);
export const getSelectorApply = () => screen.getByTestId(selectors.selector.apply);
export const getSelectorCancel = () => screen.getByTestId(selectors.selector.cancel);
export const getDashboardsExpand = () => screen.getByTestId(selectors.dashboards.expand);
export const queryDashboardsContainer = () => screen.queryByTestId(selectors.dashboards.container);
export const queryDashboardsSearch = () => screen.queryByTestId(selectors.dashboards.search);
export const getDashboardsSearch = () => screen.getByTestId<HTMLInputElement>(selectors.dashboards.search);
export const queryDashboardFolderExpand = (uid: string) =>
screen.queryByTestId(selectors.dashboards.dashboardExpand(uid));
export const getDashboardFolderExpand = (uid: string) => screen.getByTestId(selectors.dashboards.dashboardExpand(uid));
export const queryAllDashboard = (uid: string) => screen.queryAllByTestId(selectors.dashboards.dashboard(uid));
export const queryDashboard = (uid: string) => screen.queryByTestId(selectors.dashboards.dashboard(uid));
export const getDashboard = (uid: string) => screen.getByTestId(selectors.dashboards.dashboard(uid));
export const getNotFoundNoScopes = () => screen.getByTestId(selectors.dashboards.notFoundNoScopes);
export const getNotFoundForScope = () => screen.getByTestId(selectors.dashboards.notFoundForScope);
export const getNotFoundForFilter = () => screen.getByTestId(selectors.dashboards.notFoundForFilter);
export const getNotFoundForFilterClear = () => screen.getByTestId(selectors.dashboards.notFoundForFilterClear);
export const getTreeSearch = () => screen.getByTestId<HTMLInputElement>(selectors.tree.search);
export const getTreeHeadline = () => screen.getByTestId(selectors.tree.headline);
export const getResultApplicationsExpand = () => screen.getByTestId(selectors.tree.expand('applications', 'result'));
export const queryResultApplicationsGrafanaTitle = () =>
screen.queryByTestId(selectors.tree.title('applications-grafana', 'result'));
export const getResultApplicationsGrafanaTitle = () =>
screen.getByTestId(selectors.tree.title('applications-grafana', 'result'));
export const getResultApplicationsGrafanaSelect = () =>
screen.getByTestId(selectors.tree.select('applications-grafana', 'result'));
export const queryPersistedApplicationsGrafanaTitle = () =>
screen.queryByTestId(selectors.tree.title('applications-grafana', 'persisted'));
export const queryResultApplicationsMimirTitle = () =>
screen.queryByTestId(selectors.tree.title('applications-mimir', 'result'));
export const getResultApplicationsMimirTitle = () =>
screen.getByTestId(selectors.tree.title('applications-mimir', 'result'));
export const getResultApplicationsMimirSelect = () =>
screen.getByTestId(selectors.tree.select('applications-mimir', 'result'));
export const queryPersistedApplicationsMimirTitle = () =>
screen.queryByTestId(selectors.tree.title('applications-mimir', 'persisted'));
export const getPersistedApplicationsMimirTitle = () =>
screen.getByTestId(selectors.tree.title('applications-mimir', 'persisted'));
export const getPersistedApplicationsMimirSelect = () =>
screen.getByTestId(selectors.tree.select('applications-mimir', 'persisted'));
export const queryResultApplicationsCloudTitle = () =>
screen.queryByTestId(selectors.tree.title('applications-cloud', 'result'));
export const getResultApplicationsCloudSelect = () =>
screen.getByTestId(selectors.tree.select('applications-cloud', 'result'));
export const getResultApplicationsCloudExpand = () =>
screen.getByTestId(selectors.tree.expand('applications-cloud', 'result'));
export const getResultApplicationsCloudDevSelect = () =>
screen.getByTestId(selectors.tree.select('applications-cloud-dev', 'result'));
export const getResultApplicationsCloudOpsSelect = () =>
screen.getByTestId(selectors.tree.select('applications-cloud-ops', 'result'));
export const getResultCloudSelect = () => screen.getByTestId(selectors.tree.select('cloud', 'result'));
export const getResultCloudExpand = () => screen.getByTestId(selectors.tree.expand('cloud', 'result'));
export const getResultCloudDevRadio = () =>
screen.getByTestId<HTMLInputElement>(selectors.tree.radio('cloud-dev', 'result'));
export const getResultCloudOpsRadio = () =>
screen.getByTestId<HTMLInputElement>(selectors.tree.radio('cloud-ops', 'result'));

View File

@ -2129,6 +2129,8 @@
},
"scopes": {
"dashboards": {
"collapse": "Collapse",
"expand": "Expand",
"loading": "Loading dashboards",
"noResultsForFilter": "No results found for your query",
"noResultsForFilterClear": "Clear search",

View File

@ -2129,6 +2129,8 @@
},
"scopes": {
"dashboards": {
"collapse": "Cőľľäpşę",
"expand": "Ēχpäʼnđ",
"loading": "Ŀőäđįʼnģ đäşĥþőäřđş",
"noResultsForFilter": "Ńő řęşūľŧş ƒőūʼnđ ƒőř yőūř qūęřy",
"noResultsForFilterClear": "Cľęäř şęäřčĥ",