mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Scopes: Group suggested dashboards (#92212)
This commit is contained in:
parent
1dd830b9f1
commit
605bc811d2
packages/grafana-data/src/types
public
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
})
|
||||
|
@ -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,
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
32
public/app/features/scopes/internal/ScopesDashboardsTree.tsx
Normal file
32
public/app/features/scopes/internal/ScopesDashboardsTree.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
};
|
@ -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),
|
||||
}),
|
||||
};
|
||||
};
|
@ -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',
|
||||
}),
|
||||
};
|
||||
};
|
@ -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;
|
||||
}, {})
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}, {});
|
||||
}
|
||||
|
@ -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'
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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());
|
||||
}
|
84
public/app/features/scopes/tests/dashboardsApi.test.ts
Normal file
84
public/app/features/scopes/tests/dashboardsApi.test.ts
Normal 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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
687
public/app/features/scopes/tests/scopes.test.ts
Normal file
687
public/app/features/scopes/tests/scopes.test.ts
Normal 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')
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
366
public/app/features/scopes/tests/utils.test.ts
Normal file
366
public/app/features/scopes/tests/utils.test.ts
Normal 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: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
446
public/app/features/scopes/tests/utils/mocks.ts
Normal file
446
public/app/features/scopes/tests/utils/mocks.ts
Normal 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']
|
||||
);
|
92
public/app/features/scopes/tests/utils/render.tsx
Normal file
92
public/app/features/scopes/tests/utils/render.tsx
Normal 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());
|
||||
}
|
92
public/app/features/scopes/tests/utils/selectors.ts
Normal file
92
public/app/features/scopes/tests/utils/selectors.ts
Normal 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'));
|
@ -2129,6 +2129,8 @@
|
||||
},
|
||||
"scopes": {
|
||||
"dashboards": {
|
||||
"collapse": "Collapse",
|
||||
"expand": "Expand",
|
||||
"loading": "Loading dashboards",
|
||||
"noResultsForFilter": "No results found for your query",
|
||||
"noResultsForFilterClear": "Clear search",
|
||||
|
@ -2129,6 +2129,8 @@
|
||||
},
|
||||
"scopes": {
|
||||
"dashboards": {
|
||||
"collapse": "Cőľľäpşę",
|
||||
"expand": "Ēχpäʼnđ",
|
||||
"loading": "Ŀőäđįʼnģ đäşĥþőäřđş",
|
||||
"noResultsForFilter": "Ńő řęşūľŧş ƒőūʼnđ ƒőř yőūř qūęřy",
|
||||
"noResultsForFilterClear": "Cľęäř şęäřčĥ",
|
||||
|
Loading…
Reference in New Issue
Block a user