Scopes: Add recent scopes selectors (#103534)

* Add recent scopes to command palette

* Remove parent action

* Support updating select set of scopes

* Filter out currently selected scope

* Add recent scopes to drawer

* Add expandable section

* Add recent scopes component

* Only show recommended for leaf nodes

* Small style fixes

* Always write to recent after fetching new scopes

* Add feature toggle check for command palette

* Use i18n

* Remove unused prop

* Add test cases for recent scopes in selector

* Add more test cases

* Add clear test action

* Remove unused imports
This commit is contained in:
Tobias Skarhed 2025-04-17 17:00:10 +02:00 committed by GitHub
parent 6bdf161865
commit 8021dee6f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 323 additions and 18 deletions

View File

@ -0,0 +1,28 @@
import { config } from '@grafana/runtime';
import { t } from 'app/core/internationalization';
import { defaultScopesServices } from 'app/features/scopes/ScopesContextProvider';
import { CommandPaletteAction } from '../types';
import { RECENT_SCOPES_PRIORITY } from '../values';
export function getRecentScopesActions(): CommandPaletteAction[] {
if (!config.featureToggles.scopeFilters) {
return [];
}
const { scopesSelectorService } = defaultScopesServices();
const recentScopes = scopesSelectorService.getRecentScopes();
return recentScopes.map((recentScope) => {
return {
id: recentScope.map((scope) => scope.scope.spec.title).join(', '),
name: recentScope.map((scope) => scope.scope.spec.title).join(', '),
section: t('command-palette.section.recent-scopes', 'Recent scopes'),
priority: RECENT_SCOPES_PRIORITY,
perform: () => {
scopesSelectorService.changeScopes(recentScope.map((scope) => scope.scope.metadata.name));
},
};
});
}

View File

@ -5,6 +5,7 @@ import { useSelector } from 'app/types';
import { CommandPaletteAction } from '../types'; import { CommandPaletteAction } from '../types';
import { getRecentDashboardActions } from './dashboardActions'; import { getRecentDashboardActions } from './dashboardActions';
import { getRecentScopesActions } from './recentScopesActions';
import getStaticActions from './staticActions'; import getStaticActions from './staticActions';
import useExtensionActions from './useExtensionActions'; import useExtensionActions from './useExtensionActions';
@ -14,6 +15,7 @@ export default function useActions(searchQuery: string) {
const extensionActions = useExtensionActions(); const extensionActions = useExtensionActions();
const navBarTree = useSelector((state) => state.navBarTree); const navBarTree = useSelector((state) => state.navBarTree);
const recentScopesActions = getRecentScopesActions();
// Load standard static actions // Load standard static actions
useEffect(() => { useEffect(() => {
@ -32,5 +34,5 @@ export default function useActions(searchQuery: string) {
} }
}, [searchQuery]); }, [searchQuery]);
return searchQuery ? navTreeActions : [...recentDashboardActions, ...navTreeActions]; return searchQuery ? navTreeActions : [...recentDashboardActions, ...navTreeActions, ...recentScopesActions];
} }

View File

@ -1,3 +1,4 @@
export const RECENT_SCOPES_PRIORITY = 7;
export const RECENT_DASHBOARDS_PRIORITY = 6; export const RECENT_DASHBOARDS_PRIORITY = 6;
export const ACTIONS_PRIORITY = 5; export const ACTIONS_PRIORITY = 5;
export const DEFAULT_PRIORITY = 4; export const DEFAULT_PRIORITY = 4;

View File

@ -0,0 +1,79 @@
import { css } from '@emotion/css';
import { useId, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2, Stack, Text, Icon, Box } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { SelectedScope } from './types';
interface RecentScopesProps {
recentScopes: SelectedScope[][];
onSelect: (scopes: SelectedScope[]) => void;
}
export const RecentScopes = ({ recentScopes, onSelect }: RecentScopesProps) => {
const styles = useStyles2(getStyles);
const [expanded, setExpanded] = useState(false);
const contentId = useId();
return (
<fieldset>
<legend className={styles.legend}>
<button
className={styles.expandButton}
aria-expanded={expanded}
aria-controls={contentId}
onClick={() => setExpanded(!expanded)}
data-testid="scopes-selector-recent-scopes-section"
>
<Icon name={expanded ? 'angle-down' : 'angle-right'} />
<Text variant="body">
<Trans i18nKey="command-palette.section.recent-scopes" />
</Text>
</button>
</legend>
<Box paddingLeft={3} paddingTop={expanded ? 1 : 0} paddingBottom={expanded ? 1 : 0}>
<Stack direction="column" gap={1} id={contentId}>
{expanded &&
recentScopes.map((recentScopeSet) => (
<button
className={styles.recentScopeButton}
key={recentScopeSet.map((s) => s.scope.metadata.name).join(',')}
onClick={() => {
onSelect(recentScopeSet);
}}
>
<Text>{recentScopeSet.map((s) => s.scope.spec.title).join(', ')}</Text>
</button>
))}
</Stack>
</Box>
</fieldset>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
recentScopeButton: css({
textAlign: 'left',
background: 'none',
border: 'none',
padding: 0,
cursor: 'pointer',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
}),
expandButton: css({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
background: 'none',
border: 'none',
padding: 0,
cursor: 'pointer',
}),
legend: css({
marginBottom: 0,
}),
});

View File

@ -85,6 +85,7 @@ export function ScopesInput({ nodes, scopes, disabled, loading, onInputClick, on
<IconButton <IconButton
aria-label={t('scopes.selector.input.removeAll', 'Remove all scopes')} aria-label={t('scopes.selector.input.removeAll', 'Remove all scopes')}
name="times" name="times"
data-testid="scopes-selector-input-clear"
onClick={() => onRemoveAllClick()} onClick={() => onRemoveAllClick()}
/> />
) : undefined ) : undefined

View File

@ -34,7 +34,10 @@ export const ScopesSelector = () => {
const { nodes, loadingNodeName, selectedScopes, opened, treeScopes } = selectorServiceState; const { nodes, loadingNodeName, selectedScopes, opened, treeScopes } = selectorServiceState;
const { scopesService, scopesSelectorService, scopesDashboardsService } = services; const { scopesService, scopesSelectorService, scopesDashboardsService } = services;
const { readOnly, drawerOpened, loading } = scopes.state; const { readOnly, drawerOpened, loading } = scopes.state;
const { open, removeAllScopes, closeAndApply, closeAndReset, updateNode, toggleNodeSelect } = scopesSelectorService; const { open, removeAllScopes, closeAndApply, closeAndReset, updateNode, toggleNodeSelect, getRecentScopes } =
scopesSelectorService;
const recentScopes = getRecentScopes();
const dashboardsIconLabel = readOnly const dashboardsIconLabel = readOnly
? t('scopes.dashboards.toggle.disabled', 'Suggested dashboards list is disabled due to read only mode') ? t('scopes.dashboards.toggle.disabled', 'Suggested dashboards list is disabled due to read only mode')
@ -74,14 +77,21 @@ export const ScopesSelector = () => {
{loading ? ( {loading ? (
<Spinner data-testid="scopes-selector-loading" /> <Spinner data-testid="scopes-selector-loading" />
) : ( ) : (
<ScopesTree <>
nodes={nodes} <ScopesTree
nodePath={['']} nodes={nodes}
loadingNodeName={loadingNodeName} nodePath={['']}
scopes={treeScopes} loadingNodeName={loadingNodeName}
onNodeUpdate={updateNode} scopes={treeScopes}
onNodeSelectToggle={toggleNodeSelect} onNodeUpdate={updateNode}
/> onNodeSelectToggle={toggleNodeSelect}
recentScopes={recentScopes}
onRecentScopesSelect={(recentScopeSet) => {
scopesSelectorService.changeScopes(recentScopeSet.map((s) => s.scope.metadata.name));
scopesSelectorService.closeAndApply();
}}
/>
</>
)} )}
</div> </div>

View File

@ -7,6 +7,8 @@ import { getEmptyScopeObject } from '../utils';
import { NodeReason, NodesMap, SelectedScope, TreeScope } from './types'; import { NodeReason, NodesMap, SelectedScope, TreeScope } from './types';
const RECENT_SCOPES_KEY = 'grafana.scopes.recent';
export interface ScopesSelectorServiceState { export interface ScopesSelectorServiceState {
loading: boolean; loading: boolean;
@ -188,11 +190,41 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
this.dashboardsService.fetchDashboards(selectedScopes.map(({ scope }) => scope.metadata.name)); this.dashboardsService.fetchDashboards(selectedScopes.map(({ scope }) => scope.metadata.name));
selectedScopes = await this.apiClient.fetchMultipleScopes(treeScopes); selectedScopes = await this.apiClient.fetchMultipleScopes(treeScopes);
if (selectedScopes.length > 0) {
this.addRecentScopes(selectedScopes);
}
this.updateState({ selectedScopes, loading: false }); this.updateState({ selectedScopes, loading: false });
}; };
public removeAllScopes = () => this.setNewScopes([]); public removeAllScopes = () => this.setNewScopes([]);
private addRecentScopes = (scopes: SelectedScope[]) => {
if (scopes.length === 0) {
return;
}
const RECENT_SCOPES_MAX_LENGTH = 5;
const recentScopes = this.getRecentScopes();
recentScopes.unshift(scopes);
localStorage.setItem(RECENT_SCOPES_KEY, JSON.stringify(recentScopes.slice(0, RECENT_SCOPES_MAX_LENGTH - 1)));
};
public getRecentScopes = (): SelectedScope[][] => {
const recentScopes = JSON.parse(localStorage.getItem(RECENT_SCOPES_KEY) || '[]');
// TODO: Make type safe
// Filter out the current selection from recent scopes to avoid duplicates
const filteredScopes = recentScopes.filter((scopes: SelectedScope[]) => {
if (scopes.length !== this.state.selectedScopes.length) {
return true;
}
const scopeSet = new Set(scopes.map((s) => s.scope.metadata.name));
return !this.state.selectedScopes.every((s) => scopeSet.has(s.scope.metadata.name));
});
return filteredScopes.map((scopes: SelectedScope[]) => scopes);
};
/** /**
* Opens the scopes selector drawer and loads the root nodes if they are not loaded yet. * Opens the scopes selector drawer and loads the root nodes if they are not loaded yet.
*/ */

View File

@ -1,12 +1,12 @@
import { Dictionary, groupBy } from 'lodash'; import { Dictionary, groupBy } from 'lodash';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { RecentScopes } from './RecentScopes';
import { ScopesTreeHeadline } from './ScopesTreeHeadline'; import { ScopesTreeHeadline } from './ScopesTreeHeadline';
import { ScopesTreeItem } from './ScopesTreeItem'; import { ScopesTreeItem } from './ScopesTreeItem';
import { ScopesTreeLoading } from './ScopesTreeLoading'; import { ScopesTreeLoading } from './ScopesTreeLoading';
import { ScopesTreeSearch } from './ScopesTreeSearch'; import { ScopesTreeSearch } from './ScopesTreeSearch';
import { Node, NodeReason, NodesMap, OnNodeSelectToggle, OnNodeUpdate, TreeScope } from './types'; import { Node, NodeReason, NodesMap, OnNodeSelectToggle, OnNodeUpdate, TreeScope, SelectedScope } from './types';
export interface ScopesTreeProps { export interface ScopesTreeProps {
nodes: NodesMap; nodes: NodesMap;
nodePath: string[]; nodePath: string[];
@ -14,6 +14,10 @@ export interface ScopesTreeProps {
scopes: TreeScope[]; scopes: TreeScope[];
onNodeUpdate: OnNodeUpdate; onNodeUpdate: OnNodeUpdate;
onNodeSelectToggle: OnNodeSelectToggle; onNodeSelectToggle: OnNodeSelectToggle;
// Recent scopes are only shown at the root node
recentScopes?: SelectedScope[][];
onRecentScopesSelect?: (recentScopeSet: SelectedScope[]) => void;
} }
export function ScopesTree({ export function ScopesTree({
@ -21,6 +25,8 @@ export function ScopesTree({
nodePath, nodePath,
loadingNodeName, loadingNodeName,
scopes, scopes,
recentScopes,
onRecentScopesSelect,
onNodeUpdate, onNodeUpdate,
onNodeSelectToggle, onNodeSelectToggle,
}: ScopesTreeProps) { }: ScopesTreeProps) {
@ -41,6 +47,13 @@ export function ScopesTree({
query={node.query} query={node.query}
onNodeUpdate={onNodeUpdate} onNodeUpdate={onNodeUpdate}
/> />
{nodePath.length === 1 &&
nodePath[0] === '' &&
!anyChildExpanded &&
recentScopes &&
recentScopes.length > 0 &&
onRecentScopesSelect &&
!node.query && <RecentScopes recentScopes={recentScopes} onSelect={onRecentScopesSelect} />}
<ScopesTreeLoading nodeLoading={nodeLoading}> <ScopesTreeLoading nodeLoading={nodeLoading}>
<ScopesTreeItem <ScopesTreeItem

View File

@ -14,8 +14,7 @@ export interface ScopesTreeHeadlineProps {
export function ScopesTreeHeadline({ anyChildExpanded, query, resultsNodes }: ScopesTreeHeadlineProps) { export function ScopesTreeHeadline({ anyChildExpanded, query, resultsNodes }: ScopesTreeHeadlineProps) {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
if (anyChildExpanded || (resultsNodes.some((n) => n.nodeType === 'container') && !query)) {
if (anyChildExpanded) {
return null; return null;
} }

View File

@ -3,8 +3,26 @@ import { config, locationService } from '@grafana/runtime';
import { getDashboardScenePageStateManager } from '../../dashboard-scene/pages/DashboardScenePageStateManager'; import { getDashboardScenePageStateManager } from '../../dashboard-scene/pages/DashboardScenePageStateManager';
import { ScopesService } from '../ScopesService'; import { ScopesService } from '../ScopesService';
import { applyScopes, cancelScopes, openSelector, selectResultCloud, updateScopes } from './utils/actions'; import {
import { expectScopesSelectorValue } from './utils/assertions'; applyScopes,
cancelScopes,
selectResultApplicationsMimir,
selectResultApplicationsGrafana,
openSelector,
selectResultCloud,
updateScopes,
expandRecentScopes,
expandResultApplications,
selectRecentScope,
clearSelector,
} from './utils/actions';
import {
expectRecentScope,
expectRecentScopeNotPresent,
expectRecentScopeNotPresentInDocument,
expectRecentScopesSection,
expectScopesSelectorValue,
} from './utils/assertions';
import { getDatasource, getInstanceSettings, getMock, mocksScopes } from './utils/mocks'; import { getDatasource, getInstanceSettings, getMock, mocksScopes } from './utils/mocks';
import { renderDashboard, resetScenes } from './utils/render'; import { renderDashboard, resetScenes } from './utils/render';
import { getListOfScopes } from './utils/selectors'; import { getListOfScopes } from './utils/selectors';
@ -33,6 +51,7 @@ describe('Selector', () => {
scopesService = result.scopesService; scopesService = result.scopesService;
fetchSelectedScopesSpy = jest.spyOn(result.client, 'fetchMultipleScopes'); fetchSelectedScopesSpy = jest.spyOn(result.client, 'fetchMultipleScopes');
dashboardReloadSpy = jest.spyOn(getDashboardScenePageStateManager(), 'reloadDashboard'); dashboardReloadSpy = jest.spyOn(getDashboardScenePageStateManager(), 'reloadDashboard');
window.localStorage.clear();
}); });
afterEach(async () => { afterEach(async () => {
@ -65,4 +84,92 @@ describe('Selector', () => {
await updateScopes(scopesService, ['grafana']); await updateScopes(scopesService, ['grafana']);
expect(dashboardReloadSpy).not.toHaveBeenCalled(); expect(dashboardReloadSpy).not.toHaveBeenCalled();
}); });
describe('Recent scopes', () => {
it('Recent scopes should appear after selecting a second set of scopes', async () => {
await openSelector();
await expandResultApplications();
await selectResultApplicationsGrafana();
await applyScopes();
await openSelector();
await selectResultApplicationsMimir();
await applyScopes();
// Grafana,Mimir currently selected. Grafana is the first recent scope.
await openSelector();
expectRecentScopesSection();
await expandRecentScopes();
expectRecentScope('Grafana');
expectRecentScopeNotPresent('Mimir');
expectRecentScopeNotPresent('Grafana, Mimir');
await selectRecentScope('Grafana');
expectScopesSelectorValue('Grafana');
await openSelector();
await expandRecentScopes();
expectRecentScope('Grafana, Mimir');
expectRecentScopeNotPresent('Grafana');
expectRecentScopeNotPresent('Mimir');
await selectRecentScope('Grafana, Mimir');
expectScopesSelectorValue('Grafana, Mimir');
});
it('recent scopes should not be visible when the first scope is selected', async () => {
await openSelector();
await expandResultApplications();
await selectResultApplicationsGrafana();
await applyScopes();
await openSelector();
expectRecentScopeNotPresentInDocument();
});
it('should not show recent scopes when no scopes have been previously selected', async () => {
await openSelector();
expectRecentScopeNotPresentInDocument();
});
it('should maintain recent scopes after deselecting all scopes', async () => {
// First select some scopes
await openSelector();
await expandResultApplications();
await selectResultApplicationsGrafana();
await selectResultApplicationsMimir();
await applyScopes();
// Deselect all scopes
await clearSelector();
// Recent scopes should still be available
await openSelector();
expectRecentScopesSection();
await expandRecentScopes();
expectRecentScope('Grafana, Mimir');
});
it('should update recent scopes when selecting a different combination', async () => {
// First select Grafana + Mimir
await openSelector();
await expandResultApplications();
await selectResultApplicationsGrafana();
await selectResultApplicationsMimir();
await applyScopes();
// Then select just Grafana
await openSelector();
await selectResultApplicationsMimir();
await applyScopes();
await clearSelector();
// Check recent scopes are updated
await openSelector();
await expandRecentScopes();
expectRecentScope('Grafana, Mimir');
expectRecentScope('Grafana');
});
});
}); });

View File

@ -249,7 +249,6 @@ describe('Tree', () => {
it('Shows the proper headline', async () => { it('Shows the proper headline', async () => {
await openSelector(); await openSelector();
expectScopesHeadline('Recommended');
await searchScopes('Applications'); await searchScopes('Applications');
expect(fetchNodesSpy).toHaveBeenCalledTimes(2); expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
@ -260,6 +259,13 @@ describe('Tree', () => {
expectScopesHeadline('No results found for your query'); expectScopesHeadline('No results found for your query');
}); });
it('Should only show Recommended when there are no leaf container nodes visible', async () => {
await openSelector();
await expandResultApplications();
await expandResultApplicationsCloud();
expectScopesHeadline('Recommended');
});
it('Updates the paths for scopes without paths on nodes fetching', async () => { it('Updates the paths for scopes without paths on nodes fetching', async () => {
const selectedScopeName = 'grafana'; const selectedScopeName = 'grafana';
const unselectedScopeName = 'mimir'; const unselectedScopeName = 'mimir';

View File

@ -12,7 +12,10 @@ import {
getDashboardsExpand, getDashboardsExpand,
getDashboardsSearch, getDashboardsSearch,
getNotFoundForFilterClear, getNotFoundForFilterClear,
getPersistedApplicationsGrafanaSelect,
getPersistedApplicationsMimirSelect, getPersistedApplicationsMimirSelect,
getRecentScopeSet,
getRecentScopesSection,
getResultApplicationsCloudDevSelect, getResultApplicationsCloudDevSelect,
getResultApplicationsCloudExpand, getResultApplicationsCloudExpand,
getResultApplicationsCloudSelect, getResultApplicationsCloudSelect,
@ -25,6 +28,7 @@ import {
getResultCloudSelect, getResultCloudSelect,
getSelectorApply, getSelectorApply,
getSelectorCancel, getSelectorCancel,
getSelectorClear,
getSelectorInput, getSelectorInput,
getTreeSearch, getTreeSearch,
} from './selectors'; } from './selectors';
@ -38,6 +42,7 @@ const type = async (selector: () => HTMLInputElement, value: string) => {
export const updateScopes = async (service: ScopesService, scopes: string[]) => export const updateScopes = async (service: ScopesService, scopes: string[]) =>
act(async () => service.changeScopes(scopes)); act(async () => service.changeScopes(scopes));
export const openSelector = async () => click(getSelectorInput); export const openSelector = async () => click(getSelectorInput);
export const clearSelector = async () => click(getSelectorClear);
export const applyScopes = async () => { export const applyScopes = async () => {
await click(getSelectorApply); await click(getSelectorApply);
await jest.runOnlyPendingTimersAsync(); await jest.runOnlyPendingTimersAsync();
@ -45,11 +50,14 @@ export const applyScopes = async () => {
export const cancelScopes = async () => click(getSelectorCancel); export const cancelScopes = async () => click(getSelectorCancel);
export const searchScopes = async (value: string) => type(getTreeSearch, value); export const searchScopes = async (value: string) => type(getTreeSearch, value);
export const clearScopesSearch = async () => type(getTreeSearch, ''); export const clearScopesSearch = async () => type(getTreeSearch, '');
export const expandRecentScopes = async () => click(getRecentScopesSection);
export const expandResultApplications = async () => click(getResultApplicationsExpand); export const expandResultApplications = async () => click(getResultApplicationsExpand);
export const expandResultApplicationsCloud = async () => click(getResultApplicationsCloudExpand); export const expandResultApplicationsCloud = async () => click(getResultApplicationsCloudExpand);
export const expandResultCloud = async () => click(getResultCloudExpand); export const expandResultCloud = async () => click(getResultCloudExpand);
export const selectRecentScope = async (scope: string) => click(() => getRecentScopeSet(scope));
export const selectResultApplicationsGrafana = async () => click(getResultApplicationsGrafanaSelect); export const selectResultApplicationsGrafana = async () => click(getResultApplicationsGrafanaSelect);
export const selectPersistedApplicationsMimir = async () => click(getPersistedApplicationsMimirSelect); export const selectPersistedApplicationsMimir = async () => click(getPersistedApplicationsMimirSelect);
export const selectPersistedApplicationsGrafana = async () => click(getPersistedApplicationsGrafanaSelect);
export const selectResultApplicationsMimir = async () => click(getResultApplicationsMimirSelect); export const selectResultApplicationsMimir = async () => click(getResultApplicationsMimirSelect);
export const selectResultApplicationsCloud = async () => click(getResultApplicationsCloudSelect); export const selectResultApplicationsCloud = async () => click(getResultApplicationsCloudSelect);
export const selectResultApplicationsCloudDev = async () => click(getResultApplicationsCloudDevSelect); export const selectResultApplicationsCloudDev = async () => click(getResultApplicationsCloudDevSelect);

View File

@ -9,6 +9,8 @@ import {
getNotFoundForScope, getNotFoundForScope,
getNotFoundNoScopes, getNotFoundNoScopes,
getPersistedApplicationsMimirSelect, getPersistedApplicationsMimirSelect,
getRecentScopeSet,
getRecentScopesSection,
getResultApplicationsCloudSelect, getResultApplicationsCloudSelect,
getResultApplicationsGrafanaSelect, getResultApplicationsGrafanaSelect,
getResultApplicationsMimirSelect, getResultApplicationsMimirSelect,
@ -25,6 +27,8 @@ import {
queryDashboardsSearch, queryDashboardsSearch,
queryPersistedApplicationsGrafanaSelect, queryPersistedApplicationsGrafanaSelect,
queryPersistedApplicationsMimirSelect, queryPersistedApplicationsMimirSelect,
queryRecentScopeSet,
queryRecentScopesSection,
queryResultApplicationsCloudSelect, queryResultApplicationsCloudSelect,
queryResultApplicationsGrafanaSelect, queryResultApplicationsGrafanaSelect,
queryResultApplicationsMimirSelect, queryResultApplicationsMimirSelect,
@ -40,6 +44,10 @@ const expectValue = (selector: () => HTMLInputElement, value: string) => expect(
const expectTextContent = (selector: () => HTMLElement, text: string) => expect(selector()).toHaveTextContent(text); const expectTextContent = (selector: () => HTMLElement, text: string) => expect(selector()).toHaveTextContent(text);
const expectDisabled = (selector: () => HTMLElement) => expect(selector()).toBeDisabled(); const expectDisabled = (selector: () => HTMLElement) => expect(selector()).toBeDisabled();
export const expectRecentScopeNotPresent = (scope: string) => expectNotInDocument(() => queryRecentScopeSet(scope));
export const expectRecentScope = (scope: string) => expectInDocument(() => getRecentScopeSet(scope));
export const expectRecentScopeNotPresentInDocument = () => expectNotInDocument(queryRecentScopesSection);
export const expectRecentScopesSection = () => expectInDocument(getRecentScopesSection);
export const expectScopesSelectorClosed = () => expectNotInDocument(querySelectorApply); export const expectScopesSelectorClosed = () => expectNotInDocument(querySelectorApply);
export const expectScopesSelectorDisabled = () => expectDisabled(getSelectorInput); export const expectScopesSelectorDisabled = () => expectDisabled(getSelectorInput);
export const expectScopesSelectorValue = (value: string) => expectValue(getSelectorInput, value); export const expectScopesSelectorValue = (value: string) => expectValue(getSelectorInput, value);

View File

@ -5,6 +5,7 @@ import { ScopesSelectorService } from '../../selector/ScopesSelectorService';
const selectors = { const selectors = {
tree: { tree: {
recentScopesSection: 'scopes-selector-recent-scopes-section',
search: 'scopes-tree-search', search: 'scopes-tree-search',
headline: 'scopes-tree-headline', headline: 'scopes-tree-headline',
select: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-checkbox`, select: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-checkbox`,
@ -18,6 +19,7 @@ const selectors = {
loading: 'scopes-selector-loading', loading: 'scopes-selector-loading',
apply: 'scopes-selector-apply', apply: 'scopes-selector-apply',
cancel: 'scopes-selector-cancel', cancel: 'scopes-selector-cancel',
clear: 'scopes-selector-input-clear',
}, },
dashboards: { dashboards: {
expand: 'scopes-dashboards-expand', expand: 'scopes-dashboards-expand',
@ -34,10 +36,16 @@ const selectors = {
}; };
export const getSelectorInput = () => screen.getByTestId<HTMLInputElement>(selectors.selector.input); export const getSelectorInput = () => screen.getByTestId<HTMLInputElement>(selectors.selector.input);
export const getSelectorClear = () => screen.getByTestId(selectors.selector.clear);
export const querySelectorApply = () => screen.queryByTestId(selectors.selector.apply); export const querySelectorApply = () => screen.queryByTestId(selectors.selector.apply);
export const getSelectorApply = () => screen.getByTestId(selectors.selector.apply); export const getSelectorApply = () => screen.getByTestId(selectors.selector.apply);
export const getSelectorCancel = () => screen.getByTestId(selectors.selector.cancel); export const getSelectorCancel = () => screen.getByTestId(selectors.selector.cancel);
export const getRecentScopesSection = () => screen.getByTestId(selectors.tree.recentScopesSection);
export const queryRecentScopesSection = () => screen.queryByTestId(selectors.tree.recentScopesSection);
export const getRecentScopeSet = (scope: string) => screen.getByRole('button', { name: scope });
export const queryRecentScopeSet = (scope: string) => screen.queryByRole('button', { name: scope });
export const getDashboardsExpand = () => screen.getByTestId(selectors.dashboards.expand); export const getDashboardsExpand = () => screen.getByTestId(selectors.dashboards.expand);
export const getDashboardsContainer = () => screen.getByTestId(selectors.dashboards.container); export const getDashboardsContainer = () => screen.getByTestId(selectors.dashboards.container);
export const queryDashboardsContainer = () => screen.queryByTestId(selectors.dashboards.container); export const queryDashboardsContainer = () => screen.queryByTestId(selectors.dashboards.container);
@ -63,6 +71,8 @@ export const getResultApplicationsGrafanaSelect = () =>
screen.getByTestId<HTMLInputElement>(selectors.tree.select('applications-grafana', 'result')); screen.getByTestId<HTMLInputElement>(selectors.tree.select('applications-grafana', 'result'));
export const queryPersistedApplicationsGrafanaSelect = () => export const queryPersistedApplicationsGrafanaSelect = () =>
screen.queryByTestId<HTMLInputElement>(selectors.tree.select('applications-grafana', 'persisted')); screen.queryByTestId<HTMLInputElement>(selectors.tree.select('applications-grafana', 'persisted'));
export const getPersistedApplicationsGrafanaSelect = () =>
screen.getByTestId(selectors.tree.select('applications-grafana', 'persisted'));
export const queryResultApplicationsMimirSelect = () => export const queryResultApplicationsMimirSelect = () =>
screen.queryByTestId(selectors.tree.select('applications-mimir', 'result')); screen.queryByTestId(selectors.tree.select('applications-mimir', 'result'));
export const getResultApplicationsMimirSelect = () => export const getResultApplicationsMimirSelect = () =>

View File

@ -2719,7 +2719,8 @@
"folder-search-results": "Folders", "folder-search-results": "Folders",
"pages": "Pages", "pages": "Pages",
"preferences": "Preferences", "preferences": "Preferences",
"recent-dashboards": "Recent dashboards" "recent-dashboards": "Recent dashboards",
"recent-scopes": "Recent scopes"
} }
}, },
"common": { "common": {