diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx
index 7af747fc9d8..c693b626299 100644
--- a/public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx
+++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx
@@ -17,9 +17,9 @@ import { t, Trans } from 'app/core/internationalization';
import { ScopesInput } from './ScopesInput';
import { ScopesScene } from './ScopesScene';
-import { ScopesTreeLevel } from './ScopesTreeLevel';
+import { ScopesTree } from './ScopesTree';
import { fetchNodes, fetchScope, fetchSelectedScopes } from './api';
-import { NodesMap, SelectedScope, TreeScope } from './types';
+import { NodeReason, NodesMap, SelectedScope, TreeScope } from './types';
import { getBasicScope } from './utils';
export interface ScopesFiltersSceneState extends SceneObjectState {
@@ -47,6 +47,7 @@ export class ScopesFiltersScene extends SceneObjectBase
nodes: {
'': {
name: '',
+ reason: NodeReason.Result,
nodeType: 'container',
title: '',
isExpandable: true,
@@ -119,7 +120,19 @@ export class ScopesFiltersScene extends SceneObjectBase
})
)
.subscribe((childNodes) => {
- currentNode.nodes = childNodes;
+ const persistedNodes = this.state.treeScopes
+ .map(({ path }) => path[path.length - 1])
+ .filter((nodeName) => nodeName in currentNode.nodes && !(nodeName in childNodes))
+ .reduce((acc, nodeName) => {
+ acc[nodeName] = {
+ ...currentNode.nodes[nodeName],
+ reason: NodeReason.Persisted,
+ };
+
+ return acc;
+ }, {});
+
+ currentNode.nodes = { ...persistedNodes, ...childNodes };
this.setState({ nodes });
@@ -284,7 +297,7 @@ export function ScopesFiltersSceneRenderer({ model }: SceneComponentProps
) : (
- {
+ setIsTooltipVisible(false);
+ }, [scopes]);
+
const scopesPaths = useMemo(() => {
const pathsTitles = scopes.map(({ scope, path }) => {
let currentLevel = nodes;
@@ -64,7 +69,7 @@ export function ScopesInput({
const groupedByPath = groupBy(pathsTitles, ([path]) => path);
- return Object.entries(groupedByPath)
+ const scopesPaths = Object.entries(groupedByPath)
.map(([path, pathScopes]) => {
const scopesTitles = pathScopes.map(([, scopeTitle]) => scopeTitle).join(', ');
@@ -75,41 +80,44 @@ export function ScopesInput({
{path}
));
+
+ return <>{scopesPaths}>;
}, [nodes, scopes, styles]);
const scopesTitles = useMemo(() => scopes.map(({ scope }) => scope.spec.title).join(', '), [scopes]);
- const input = (
- 0 && !isDisabled ? (
- onRemoveAllClick()}
- />
- ) : undefined
- }
- onClick={() => {
- if (!isDisabled) {
- onInputClick();
+ const input = useMemo(
+ () => (
+ 0 && !isDisabled ? (
+ onRemoveAllClick()}
+ />
+ ) : undefined
}
- }}
- />
+ onMouseOver={() => setIsTooltipVisible(true)}
+ onMouseOut={() => setIsTooltipVisible(false)}
+ onClick={() => {
+ if (!isDisabled) {
+ onInputClick();
+ }
+ }}
+ />
+ ),
+ [isDisabled, isLoading, onInputClick, onRemoveAllClick, scopes, scopesTitles]
);
- if (scopes.length === 0) {
- return input;
- }
-
return (
- {scopesPaths}>} interactive={true}>
+
{input}
);
diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx
index 84cea5de184..0f4fb4d188e 100644
--- a/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx
+++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx
@@ -10,47 +10,54 @@ import { ScopesFiltersScene } from './ScopesFiltersScene';
import { ScopesScene } from './ScopesScene';
import {
buildTestScene,
- fetchSuggestedDashboardsSpy,
fetchNodesSpy,
fetchScopeSpy,
fetchSelectedScopesSpy,
- getApplicationsClustersExpand,
- getApplicationsClustersSelect,
- getApplicationsClustersSlothClusterNorthSelect,
- getApplicationsClustersSlothClusterSouthSelect,
- getApplicationsExpand,
- getApplicationsSearch,
- getApplicationsSlothPictureFactorySelect,
- getApplicationsSlothPictureFactoryTitle,
- getApplicationsSlothVoteTrackerSelect,
- getFiltersApply,
- getFiltersCancel,
- getFiltersInput,
- getClustersExpand,
- getClustersSelect,
- getClustersSlothClusterNorthRadio,
- getClustersSlothClusterSouthRadio,
+ fetchSuggestedDashboardsSpy,
getDashboard,
getDashboardsContainer,
getDashboardsExpand,
getDashboardsSearch,
+ getFiltersApply,
+ getFiltersCancel,
+ getFiltersInput,
getMock,
+ getNotFoundForFilter,
+ getNotFoundForFilterClear,
+ getNotFoundForScope,
+ getNotFoundNoScopes,
+ getPersistedApplicationsSlothPictureFactorySelect,
+ getPersistedApplicationsSlothPictureFactoryTitle,
+ getPersistedApplicationsSlothVoteTrackerTitle,
+ getResultApplicationsClustersExpand,
+ getResultApplicationsClustersSelect,
+ getResultApplicationsClustersSlothClusterNorthSelect,
+ getResultApplicationsClustersSlothClusterSouthSelect,
+ getResultApplicationsExpand,
+ getResultApplicationsSlothPictureFactorySelect,
+ getResultApplicationsSlothPictureFactoryTitle,
+ getResultApplicationsSlothVoteTrackerSelect,
+ getResultApplicationsSlothVoteTrackerTitle,
+ getResultClustersExpand,
+ getResultClustersSelect,
+ getResultClustersSlothClusterEastRadio,
+ getResultClustersSlothClusterNorthRadio,
+ getResultClustersSlothClusterSouthRadio,
+ getTreeHeadline,
+ getTreeSearch,
mocksScopes,
queryAllDashboard,
- queryFiltersApply,
- queryApplicationsClustersTitle,
- queryApplicationsSlothPictureFactoryTitle,
- queryApplicationsSlothVoteTrackerTitle,
queryDashboard,
queryDashboardsContainer,
queryDashboardsExpand,
- renderDashboard,
- getNotFoundForScope,
queryDashboardsSearch,
- getNotFoundForFilter,
- getClustersSlothClusterEastRadio,
- getNotFoundForFilterClear,
- getNotFoundNoScopes,
+ queryFiltersApply,
+ queryPersistedApplicationsSlothPictureFactoryTitle,
+ queryPersistedApplicationsSlothVoteTrackerTitle,
+ queryResultApplicationsClustersTitle,
+ queryResultApplicationsSlothPictureFactoryTitle,
+ queryResultApplicationsSlothVoteTrackerTitle,
+ renderDashboard,
} from './testUtils';
jest.mock('@grafana/runtime', () => ({
@@ -106,15 +113,15 @@ describe('ScopesScene', () => {
describe('Tree', () => {
it('Navigates through scopes nodes', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsClustersExpand());
- await userEvents.click(getApplicationsExpand());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsClustersExpand());
+ await userEvents.click(getResultApplicationsExpand());
});
it('Fetches scope details on select', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothVoteTrackerSelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
await waitFor(() => expect(fetchScopeSpy).toHaveBeenCalledTimes(1));
});
@@ -126,77 +133,167 @@ describe('ScopesScene', () => {
])
);
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- expect(getApplicationsSlothVoteTrackerSelect()).toBeChecked();
- expect(getApplicationsSlothPictureFactorySelect()).toBeChecked();
+ await userEvents.click(getResultApplicationsExpand());
+ expect(getResultApplicationsSlothVoteTrackerSelect()).toBeChecked();
+ expect(getResultApplicationsSlothPictureFactorySelect()).toBeChecked();
});
it('Can select scopes from same level', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothVoteTrackerSelect());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
- await userEvents.click(getApplicationsClustersSelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsClustersSelect());
await userEvents.click(getFiltersApply());
expect(getFiltersInput().value).toBe('slothVoteTracker, slothPictureFactory, Cluster Index Helper');
});
it('Can select a node from an inner level', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothVoteTrackerSelect());
- await userEvents.click(getApplicationsClustersExpand());
- await userEvents.click(getApplicationsClustersSlothClusterNorthSelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
+ await userEvents.click(getResultApplicationsClustersExpand());
+ await userEvents.click(getResultApplicationsClustersSlothClusterNorthSelect());
await userEvents.click(getFiltersApply());
expect(getFiltersInput().value).toBe('slothClusterNorth');
});
it('Can select a node from an upper level', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothVoteTrackerSelect());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getClustersSelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultClustersSelect());
await userEvents.click(getFiltersApply());
expect(getFiltersInput().value).toBe('Cluster Index Helper');
});
it('Respects only one select per container', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getClustersExpand());
- await userEvents.click(getClustersSlothClusterNorthRadio());
- expect(getClustersSlothClusterNorthRadio().checked).toBe(true);
- expect(getClustersSlothClusterSouthRadio().checked).toBe(false);
- await userEvents.click(getClustersSlothClusterSouthRadio());
- expect(getClustersSlothClusterNorthRadio().checked).toBe(false);
- expect(getClustersSlothClusterSouthRadio().checked).toBe(true);
+ 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(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.type(getApplicationsSearch(), 'Clusters');
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.type(getTreeSearch(), 'Clusters');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
- expect(queryApplicationsSlothPictureFactoryTitle()).not.toBeInTheDocument();
- expect(queryApplicationsSlothVoteTrackerTitle()).not.toBeInTheDocument();
- expect(getApplicationsClustersSelect()).toBeInTheDocument();
- await userEvents.clear(getApplicationsSearch());
- await userEvents.type(getApplicationsSearch(), 'sloth');
+ 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(getApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
- expect(getApplicationsSlothVoteTrackerSelect()).toBeInTheDocument();
- expect(queryApplicationsClustersTitle()).not.toBeInTheDocument();
+ expect(getResultApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
+ expect(getResultApplicationsSlothVoteTrackerSelect()).toBeInTheDocument();
+ expect(queryResultApplicationsClustersTitle()).not.toBeInTheDocument();
});
it('Opens to a selected scope', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getClustersExpand());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultClustersExpand());
await userEvents.click(getFiltersApply());
await userEvents.click(getFiltersInput());
- expect(queryApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
+ expect(queryResultApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
+ });
+
+ it('Persists a scope', async () => {
+ await userEvents.click(getFiltersInput());
+ 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(getFiltersInput());
+ 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(getFiltersInput());
+ 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(getFiltersInput());
+ 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(getFiltersInput());
+ 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(getFiltersApply());
+ expect(getFiltersInput().value).toBe('slothPictureFactory, slothVoteTracker');
+ });
+
+ it('Deselects a persisted scope', async () => {
+ await userEvents.click(getFiltersInput());
+ 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(getFiltersApply());
+ expect(getFiltersInput().value).toBe('slothPictureFactory, slothVoteTracker');
+ await userEvents.click(getFiltersInput());
+ await userEvents.click(getPersistedApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getFiltersApply());
+ expect(getFiltersInput().value).toBe('slothVoteTracker');
+ });
+
+ it('Shows the proper headline', async () => {
+ await userEvents.click(getFiltersInput());
+ 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');
});
});
@@ -208,7 +305,7 @@ describe('ScopesScene', () => {
it('Fetches scope details on save', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getClustersSelect());
+ await userEvents.click(getResultClustersSelect());
await userEvents.click(getFiltersApply());
await waitFor(() => expect(fetchSelectedScopesSpy).toHaveBeenCalled());
expect(filtersScene.getSelectedScopes()).toEqual(
@@ -218,7 +315,7 @@ describe('ScopesScene', () => {
it("Doesn't save the scopes on close", async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getClustersSelect());
+ await userEvents.click(getResultClustersSelect());
await userEvents.click(getFiltersCancel());
await waitFor(() => expect(fetchSelectedScopesSpy).not.toHaveBeenCalled());
expect(filtersScene.getSelectedScopes()).toEqual([]);
@@ -226,7 +323,7 @@ describe('ScopesScene', () => {
it('Shows selected scopes', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getClustersSelect());
+ await userEvents.click(getResultClustersSelect());
await userEvents.click(getFiltersApply());
expect(getFiltersInput().value).toEqual('Cluster Index Helper');
});
@@ -240,8 +337,8 @@ describe('ScopesScene', () => {
it('Does not fetch dashboards list when the list is not expanded', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
await waitFor(() => expect(fetchSuggestedDashboardsSpy).not.toHaveBeenCalled());
});
@@ -249,16 +346,16 @@ describe('ScopesScene', () => {
it('Fetches dashboards list when the list is expanded', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
await waitFor(() => expect(fetchSuggestedDashboardsSpy).toHaveBeenCalled());
});
it('Fetches dashboards list when the list is expanded after scope selection', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
await userEvents.click(getDashboardsExpand());
await waitFor(() => expect(fetchSuggestedDashboardsSpy).toHaveBeenCalled());
@@ -267,22 +364,22 @@ describe('ScopesScene', () => {
it('Shows dashboards for multiple scopes', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
expect(getDashboard('1')).toBeInTheDocument();
expect(getDashboard('2')).toBeInTheDocument();
expect(queryDashboard('3')).not.toBeInTheDocument();
expect(queryDashboard('4')).not.toBeInTheDocument();
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsSlothVoteTrackerSelect());
+ await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
await userEvents.click(getFiltersApply());
expect(getDashboard('1')).toBeInTheDocument();
expect(getDashboard('2')).toBeInTheDocument();
expect(getDashboard('3')).toBeInTheDocument();
expect(getDashboard('4')).toBeInTheDocument();
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
expect(queryDashboard('1')).not.toBeInTheDocument();
expect(queryDashboard('2')).not.toBeInTheDocument();
@@ -293,8 +390,8 @@ describe('ScopesScene', () => {
it('Filters the dashboards list', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
expect(getDashboard('1')).toBeInTheDocument();
expect(getDashboard('2')).toBeInTheDocument();
@@ -305,10 +402,10 @@ describe('ScopesScene', () => {
it('Deduplicates the dashboards list', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsClustersExpand());
- await userEvents.click(getApplicationsClustersSlothClusterNorthSelect());
- await userEvents.click(getApplicationsClustersSlothClusterSouthSelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsClustersExpand());
+ await userEvents.click(getResultApplicationsClustersSlothClusterNorthSelect());
+ await userEvents.click(getResultApplicationsClustersSlothClusterSouthSelect());
await userEvents.click(getFiltersApply());
expect(queryAllDashboard('5')).toHaveLength(1);
expect(queryAllDashboard('6')).toHaveLength(1);
@@ -325,8 +422,8 @@ describe('ScopesScene', () => {
it('Does not show the input when there are no dashboards found for scope', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getFiltersInput());
- await userEvents.click(getClustersExpand());
- await userEvents.click(getClustersSlothClusterEastRadio());
+ await userEvents.click(getResultClustersExpand());
+ await userEvents.click(getResultClustersSlothClusterEastRadio());
await userEvents.click(getFiltersApply());
expect(getNotFoundForScope()).toBeInTheDocument();
expect(queryDashboardsSearch()).not.toBeInTheDocument();
@@ -335,8 +432,8 @@ describe('ScopesScene', () => {
it('Does show the input and a message when there are no dashboards found for filter', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
await userEvents.type(getDashboardsSearch(), 'unknown');
expect(queryDashboardsSearch()).toBeInTheDocument();
@@ -380,8 +477,8 @@ describe('ScopesScene', () => {
describe('Enrichers', () => {
it('Data requests', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
await waitFor(() => {
const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!;
@@ -391,7 +488,7 @@ describe('ScopesScene', () => {
});
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsSlothVoteTrackerSelect());
+ await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
await userEvents.click(getFiltersApply());
await waitFor(() => {
const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!;
@@ -403,7 +500,7 @@ describe('ScopesScene', () => {
});
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
await waitFor(() => {
const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!;
@@ -415,8 +512,8 @@ describe('ScopesScene', () => {
it('Filters requests', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
await waitFor(() => {
expect(dashboardScene.enrichFiltersRequest().scopes).toEqual(
@@ -425,7 +522,7 @@ describe('ScopesScene', () => {
});
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsSlothVoteTrackerSelect());
+ await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
await userEvents.click(getFiltersApply());
await waitFor(() => {
expect(dashboardScene.enrichFiltersRequest().scopes).toEqual(
@@ -436,7 +533,7 @@ describe('ScopesScene', () => {
});
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
await waitFor(() => {
expect(dashboardScene.enrichFiltersRequest().scopes).toEqual(
diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTree.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesTree.tsx
new file mode 100644
index 00000000000..6f97a7e9b83
--- /dev/null
+++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesTree.tsx
@@ -0,0 +1,81 @@
+import { groupBy } from 'lodash';
+import { useMemo } from 'react';
+
+import { ScopesTreeHeadline } from './ScopesTreeHeadline';
+import { ScopesTreeItem } from './ScopesTreeItem';
+import { ScopesTreeLoading } from './ScopesTreeLoading';
+import { ScopesTreeSearch } from './ScopesTreeSearch';
+import { NodeReason, NodesMap, OnNodeSelectToggle, OnNodeUpdate, TreeScope } from './types';
+
+export interface ScopesTreeProps {
+ nodes: NodesMap;
+ nodePath: string[];
+ loadingNodeName: string | undefined;
+ scopes: TreeScope[];
+ onNodeUpdate: OnNodeUpdate;
+ onNodeSelectToggle: OnNodeSelectToggle;
+}
+
+export function ScopesTree({
+ nodes,
+ nodePath,
+ loadingNodeName,
+ scopes,
+ onNodeUpdate,
+ onNodeSelectToggle,
+}: ScopesTreeProps) {
+ const nodeId = nodePath[nodePath.length - 1];
+ const node = nodes[nodeId];
+ const childNodes = Object.values(node.nodes);
+ const isNodeLoading = loadingNodeName === nodeId;
+ const scopeNames = scopes.map(({ scopeName }) => scopeName);
+ const anyChildExpanded = childNodes.some(({ isExpanded }) => isExpanded);
+ const groupedNodes = useMemo(() => groupBy(childNodes, 'reason'), [childNodes]);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeHeadline.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeHeadline.tsx
new file mode 100644
index 00000000000..8dda39062b8
--- /dev/null
+++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeHeadline.tsx
@@ -0,0 +1,42 @@
+import { css } from '@emotion/css';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { useStyles2 } from '@grafana/ui';
+import { Trans } from 'app/core/internationalization';
+
+import { Node } from './types';
+
+export interface ScopesTreeHeadlineProps {
+ anyChildExpanded: boolean;
+ query: string;
+ resultsNodes: Node[];
+}
+
+export function ScopesTreeHeadline({ anyChildExpanded, query, resultsNodes }: ScopesTreeHeadlineProps) {
+ const styles = useStyles2(getStyles);
+
+ if (anyChildExpanded) {
+ return null;
+ }
+
+ return (
+
+ {!query ? (
+ Recommended
+ ) : resultsNodes.length === 0 ? (
+ No results found for your query
+ ) : (
+ Results
+ )}
+
+ );
+}
+
+const getStyles = (theme: GrafanaTheme2) => {
+ return {
+ container: css({
+ color: theme.colors.text.secondary,
+ margin: theme.spacing(1, 0),
+ }),
+ };
+};
diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeItem.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeItem.tsx
new file mode 100644
index 00000000000..611e0851a68
--- /dev/null
+++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeItem.tsx
@@ -0,0 +1,143 @@
+import { css } from '@emotion/css';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { Checkbox, Icon, RadioButtonDot, useStyles2 } from '@grafana/ui';
+import { t } from 'app/core/internationalization';
+
+import { ScopesTree } from './ScopesTree';
+import { Node, OnNodeSelectToggle, OnNodeUpdate, TreeScope } from './types';
+
+export interface ScopesTreeItemProps {
+ anyChildExpanded: boolean;
+ isNodeLoading: boolean;
+ loadingNodeName: string | undefined;
+ node: Node;
+ nodePath: string[];
+ nodes: Node[];
+ scopeNames: string[];
+ scopes: TreeScope[];
+ type: 'persisted' | 'result';
+ onNodeUpdate: OnNodeUpdate;
+ onNodeSelectToggle: OnNodeSelectToggle;
+}
+
+export function ScopesTreeItem({
+ anyChildExpanded,
+ loadingNodeName,
+ node,
+ nodePath,
+ nodes,
+ scopeNames,
+ scopes,
+ type,
+ onNodeSelectToggle,
+ onNodeUpdate,
+}: ScopesTreeItemProps) {
+ const styles = useStyles2(getStyles);
+
+ return (
+
+ {nodes.map((childNode) => {
+ const isSelected = childNode.isSelectable && scopeNames.includes(childNode.linkId!);
+
+ if (anyChildExpanded && !childNode.isExpanded) {
+ return null;
+ }
+
+ const childNodePath = [...nodePath, childNode.name];
+
+ const radioName = childNodePath.join('.');
+
+ return (
+
+
+ {childNode.isSelectable && !childNode.isExpanded ? (
+ node.disableMultiSelect ? (
+ {
+ onNodeSelectToggle(childNodePath);
+ }}
+ />
+ ) : (
+ {
+ onNodeSelectToggle(childNodePath);
+ }}
+ />
+ )
+ ) : null}
+
+ {childNode.isExpandable ? (
+
+ ) : (
+ {childNode.title}
+ )}
+
+
+
+ {childNode.isExpanded && (
+
+ )}
+
+
+ );
+ })}
+
+ );
+}
+
+const getStyles = (theme: GrafanaTheme2) => {
+ return {
+ title: css({
+ alignItems: 'center',
+ display: 'flex',
+ gap: theme.spacing(1),
+ fontSize: theme.typography.pxToRem(14),
+ lineHeight: theme.typography.pxToRem(22),
+ padding: theme.spacing(0.5, 0),
+
+ '& > label': css({
+ gap: 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),
+ }),
+ };
+};
diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLevel.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLevel.tsx
deleted file mode 100644
index 8d57b74d825..00000000000
--- a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLevel.tsx
+++ /dev/null
@@ -1,185 +0,0 @@
-import { css } from '@emotion/css';
-import { debounce } from 'lodash';
-import { useEffect, useMemo, useState } from 'react';
-import Skeleton from 'react-loading-skeleton';
-
-import { GrafanaTheme2 } from '@grafana/data';
-import { Checkbox, FilterInput, Icon, RadioButtonDot, useStyles2 } from '@grafana/ui';
-import { t, Trans } from 'app/core/internationalization';
-
-import { NodesMap, TreeScope } from './types';
-
-export interface ScopesTreeLevelProps {
- nodes: NodesMap;
- nodePath: string[];
- loadingNodeName: string | undefined;
- scopes: TreeScope[];
- onNodeUpdate: (path: string[], isExpanded: boolean, query: string) => void;
- onNodeSelectToggle: (path: string[]) => void;
-}
-
-export function ScopesTreeLevel({
- nodes,
- nodePath,
- loadingNodeName,
- scopes,
- onNodeUpdate,
- onNodeSelectToggle,
-}: ScopesTreeLevelProps) {
- const styles = useStyles2(getStyles);
-
- const nodeId = nodePath[nodePath.length - 1];
- const node = nodes[nodeId];
- const childNodes = node.nodes;
- const childNodesArr = Object.values(childNodes);
- const isNodeLoading = loadingNodeName === nodeId;
-
- const scopeNames = scopes.map(({ scopeName }) => scopeName);
- const anyChildExpanded = childNodesArr.some(({ isExpanded }) => isExpanded);
-
- const [queryValue, setQueryValue] = useState(node.query);
- useEffect(() => {
- setQueryValue(node.query);
- }, [node.query]);
- const onQueryUpdate = useMemo(() => debounce(onNodeUpdate, 500), [onNodeUpdate]);
-
- return (
- <>
- {!anyChildExpanded && (
- {
- setQueryValue(value);
- onQueryUpdate(nodePath, true, value);
- }}
- />
- )}
-
- {!anyChildExpanded && !node.query && (
-
- Recommended
-
- )}
-
-
- {isNodeLoading &&
}
-
- {!isNodeLoading &&
- childNodesArr.map((childNode) => {
- const isSelected = childNode.isSelectable && scopeNames.includes(childNode.linkId!);
-
- if (anyChildExpanded && !childNode.isExpanded && !isSelected) {
- return null;
- }
-
- const childNodePath = [...nodePath, childNode.name];
-
- const radioName = childNodePath.join('.');
-
- return (
-
-
- {childNode.isSelectable && !childNode.isExpanded ? (
- node.disableMultiSelect ? (
- {
- onNodeSelectToggle(childNodePath);
- }}
- />
- ) : (
- {
- onNodeSelectToggle(childNodePath);
- }}
- />
- )
- ) : null}
-
- {childNode.isExpandable ? (
-
- ) : (
- {childNode.title}
- )}
-
-
-
- {childNode.isExpanded && (
-
- )}
-
-
- );
- })}
-
- >
- );
-}
-
-const getStyles = (theme: GrafanaTheme2) => {
- return {
- searchInput: css({
- margin: theme.spacing(1, 0),
- }),
- headline: css({
- color: theme.colors.text.secondary,
- margin: theme.spacing(1, 0),
- }),
- loader: css({
- margin: theme.spacing(0.5, 0),
- }),
- itemTitle: css({
- alignItems: 'center',
- display: 'flex',
- gap: theme.spacing(1),
- fontSize: theme.typography.pxToRem(14),
- lineHeight: theme.typography.pxToRem(22),
- padding: theme.spacing(0.5, 0),
-
- '& > label': css({
- gap: 0,
- }),
- }),
- itemExpand: css({
- alignItems: 'center',
- background: 'none',
- border: 0,
- display: 'flex',
- gap: theme.spacing(1),
- margin: 0,
- padding: 0,
- }),
- itemChildren: css({
- paddingLeft: theme.spacing(4),
- }),
- };
-};
diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLoading.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLoading.tsx
new file mode 100644
index 00000000000..9a27b6a2311
--- /dev/null
+++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLoading.tsx
@@ -0,0 +1,29 @@
+import { css } from '@emotion/css';
+import { ReactNode } from 'react';
+import Skeleton from 'react-loading-skeleton';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { useStyles2 } from '@grafana/ui';
+
+export interface ScopesTreeLoadingProps {
+ children: ReactNode;
+ isNodeLoading: boolean;
+}
+
+export function ScopesTreeLoading({ children, isNodeLoading }: ScopesTreeLoadingProps) {
+ const styles = useStyles2(getStyles);
+
+ if (isNodeLoading) {
+ return ;
+ }
+
+ return children;
+}
+
+const getStyles = (theme: GrafanaTheme2) => {
+ return {
+ loader: css({
+ margin: theme.spacing(0.5, 0),
+ }),
+ };
+};
diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeSearch.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeSearch.tsx
new file mode 100644
index 00000000000..cc9f4a61d8c
--- /dev/null
+++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeSearch.tsx
@@ -0,0 +1,53 @@
+import { css } from '@emotion/css';
+import { debounce } from 'lodash';
+import { useEffect, useMemo, useState } from 'react';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { FilterInput, useStyles2 } from '@grafana/ui';
+import { t } from 'app/core/internationalization';
+
+import { OnNodeUpdate } from './types';
+
+export interface ScopesTreeSearchProps {
+ anyChildExpanded: boolean;
+ nodePath: string[];
+ query: string;
+ onNodeUpdate: OnNodeUpdate;
+}
+
+export function ScopesTreeSearch({ anyChildExpanded, nodePath, query, onNodeUpdate }: ScopesTreeSearchProps) {
+ const styles = useStyles2(getStyles);
+
+ const [queryValue, setQueryValue] = useState(query);
+
+ useEffect(() => {
+ setQueryValue(query);
+ }, [query]);
+
+ const onQueryUpdate = useMemo(() => debounce(onNodeUpdate, 500), [onNodeUpdate]);
+
+ if (anyChildExpanded) {
+ return null;
+ }
+
+ return (
+ {
+ setQueryValue(value);
+ onQueryUpdate(nodePath, true, value);
+ }}
+ />
+ );
+}
+
+const getStyles = (theme: GrafanaTheme2) => {
+ return {
+ input: css({
+ margin: theme.spacing(1, 0),
+ }),
+ };
+};
diff --git a/public/app/features/dashboard-scene/scene/Scopes/api.ts b/public/app/features/dashboard-scene/scene/Scopes/api.ts
index 07df4877a54..d1deda21d73 100644
--- a/public/app/features/dashboard-scene/scene/Scopes/api.ts
+++ b/public/app/features/dashboard-scene/scene/Scopes/api.ts
@@ -1,8 +1,8 @@
-import { Scope, ScopeSpec, ScopeNode, ScopeDashboardBinding } from '@grafana/data';
+import { Scope, ScopeDashboardBinding, ScopeNode, ScopeSpec } from '@grafana/data';
import { config, getBackendSrv } from '@grafana/runtime';
import { ScopedResourceClient } from 'app/features/apiserver/client';
-import { NodesMap, SelectedScope, SuggestedDashboard, TreeScope } from './types';
+import { NodeReason, NodesMap, SelectedScope, SuggestedDashboard, TreeScope } from './types';
import { getBasicScope, mergeScopes } from './utils';
const group = 'scope.grafana.app';
@@ -37,6 +37,7 @@ export async function fetchNodes(parent: string, query: string): Promise `scopes-tree-${nodeId}-search`,
- select: (nodeId: string) => `scopes-tree-${nodeId}-checkbox`,
- radio: (nodeId: string) => `scopes-tree-${nodeId}-radio`,
- expand: (nodeId: string) => `scopes-tree-${nodeId}-expand`,
- title: (nodeId: string) => `scopes-tree-${nodeId}-title`,
+ 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`,
},
filters: {
input: 'scopes-filters-input',
@@ -359,36 +360,50 @@ export const getNotFoundForScope = () => screen.getByTestId(selectors.dashboards
export const getNotFoundForFilter = () => screen.getByTestId(selectors.dashboards.notFoundForFilter);
export const getNotFoundForFilterClear = () => screen.getByTestId(selectors.dashboards.notFoundForFilterClear);
-export const getApplicationsExpand = () => screen.getByTestId(selectors.tree.expand('applications'));
-export const getApplicationsSearch = () => screen.getByTestId(selectors.tree.search('applications'));
-export const queryApplicationsSlothPictureFactoryTitle = () =>
- screen.queryByTestId(selectors.tree.title('applications-slothPictureFactory'));
-export const getApplicationsSlothPictureFactoryTitle = () =>
- screen.getByTestId(selectors.tree.title('applications-slothPictureFactory'));
-export const getApplicationsSlothPictureFactorySelect = () =>
- screen.getByTestId(selectors.tree.select('applications-slothPictureFactory'));
-export const queryApplicationsSlothVoteTrackerTitle = () =>
- screen.queryByTestId(selectors.tree.title('applications-slothVoteTracker'));
-export const getApplicationsSlothVoteTrackerSelect = () =>
- screen.getByTestId(selectors.tree.select('applications-slothVoteTracker'));
-export const queryApplicationsClustersTitle = () => screen.queryByTestId(selectors.tree.title('applications.clusters'));
-export const getApplicationsClustersSelect = () => screen.getByTestId(selectors.tree.select('applications.clusters'));
-export const getApplicationsClustersExpand = () => screen.getByTestId(selectors.tree.expand('applications.clusters'));
-export const queryApplicationsClustersSlothClusterNorthTitle = () =>
- screen.queryByTestId(selectors.tree.title('applications.clusters-slothClusterNorth'));
-export const getApplicationsClustersSlothClusterNorthSelect = () =>
- screen.getByTestId(selectors.tree.select('applications.clusters-slothClusterNorth'));
-export const getApplicationsClustersSlothClusterSouthSelect = () =>
- screen.getByTestId(selectors.tree.select('applications.clusters-slothClusterSouth'));
+export const getTreeSearch = () => screen.getByTestId(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 getClustersSelect = () => screen.getByTestId(selectors.tree.select('clusters'));
-export const getClustersExpand = () => screen.getByTestId(selectors.tree.expand('clusters'));
-export const getClustersSlothClusterNorthRadio = () =>
- screen.getByTestId(selectors.tree.radio('clusters-slothClusterNorth'));
-export const getClustersSlothClusterSouthRadio = () =>
- screen.getByTestId(selectors.tree.radio('clusters-slothClusterSouth'));
-export const getClustersSlothClusterEastRadio = () =>
- screen.getByTestId(selectors.tree.radio('clusters-slothClusterEast'));
+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(selectors.tree.radio('clusters-slothClusterNorth', 'result'));
+export const getResultClustersSlothClusterSouthRadio = () =>
+ screen.getByTestId(selectors.tree.radio('clusters-slothClusterSouth', 'result'));
+export const getResultClustersSlothClusterEastRadio = () =>
+ screen.getByTestId(selectors.tree.radio('clusters-slothClusterEast', 'result'));
export function buildTestScene(overrides: Partial = {}) {
return new DashboardScene({
diff --git a/public/app/features/dashboard-scene/scene/Scopes/types.ts b/public/app/features/dashboard-scene/scene/Scopes/types.ts
index 5dc7cfc1e5d..40d3cbbefae 100644
--- a/public/app/features/dashboard-scene/scene/Scopes/types.ts
+++ b/public/app/features/dashboard-scene/scene/Scopes/types.ts
@@ -1,7 +1,13 @@
import { Scope, ScopeDashboardBinding, ScopeNodeSpec } from '@grafana/data';
+export enum NodeReason {
+ Persisted,
+ Result,
+}
+
export interface Node extends ScopeNodeSpec {
name: string;
+ reason: NodeReason;
isExpandable: boolean;
isSelectable: boolean;
isExpanded: boolean;
@@ -26,3 +32,6 @@ export interface SuggestedDashboard {
dashboardTitle: string;
items: ScopeDashboardBinding[];
}
+
+export type OnNodeUpdate = (path: string[], isExpanded: boolean, query: string) => void;
+export type OnNodeSelectToggle = (path: string[]) => void;
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 70a894d96fd..92d54e9c3ae 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -1698,7 +1698,11 @@
"tree": {
"collapse": "Collapse",
"expand": "Expand",
- "headline": "Recommended",
+ "headline": {
+ "noResults": "No results found for your query",
+ "recommended": "Recommended",
+ "results": "Results"
+ },
"search": "Search"
}
},
diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json
index 3d992a19570..9a3df4cb747 100644
--- a/public/locales/pseudo-LOCALE/grafana.json
+++ b/public/locales/pseudo-LOCALE/grafana.json
@@ -1698,7 +1698,11 @@
"tree": {
"collapse": "Cőľľäpşę",
"expand": "Ēχpäʼnđ",
- "headline": "Ŗęčőmmęʼnđęđ",
+ "headline": {
+ "noResults": "Ńő řęşūľŧş ƒőūʼnđ ƒőř yőūř qūęřy",
+ "recommended": "Ŗęčőmmęʼnđęđ",
+ "results": "Ŗęşūľŧş"
+ },
"search": "Ŝęäřčĥ"
}
},