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": "Ŝęäřčĥ" } },